src/line.bend

ezmcp@0.1.0.0 · 221 lines · 29 definitions

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.
5import Base
6import emerging-ezjson@1.2.0.0/main.bend as Json
7import emerging-ezjson@1.2.0.0/src/value.bend as Value
8import ezmcp@0.1.0.0/src/server.bend as Server
9
10# whole lines, each with its newline, and the bytes after the last newline
11type Cut is Data:
12  Cut{lines: List<&2, List<&2, U32>>, rest: List<&2, U32>}
13
14# a byte is a newline
15def nl(+byte: U32) -> Bool:
16  U32.is_eq(byte, 10)
17
18# put a byte at the front of the first piece
19def 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
28def 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
41def 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
48def cut.cons(piece: List<&2, U32>, cb: Cut) -> Cut:
49  match cb:
50    case Cut{lines, rest}:
51      Cut{piece <> lines, rest}
52
53def 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
60def 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
68def 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
74def 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
78def bad() -> Char:
79  Chr{65533}
80
81# a continuation byte's six payload bits
82def low6(+byte: U32) -> U32:
83  U32.sub(byte, 128)
84
85# the code point of a two-byte sequence
86def 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
90def 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
94def 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
99type 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
107def 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
117def 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
130def 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
137def 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
149def 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
158def 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
176def 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
185def 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
193def decode(+bs: List<&2, U32>) -> String:
194  decode.go(List.length(&2, U32, bs), bs)
195
196# --- messages ---
197
198def 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
210def 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
216def 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
220def frame(val: Value.Json) -> String:
221  Json.print(val) ++ "\n"