encoding.bend
bend-kit-encoding@0.3.0.0 · 143 lines · 0 definitions
1# UTF-8 encoding and decoding between text and Bytes. Source: https://github.com/paymog/bend-kit/tree/main/encoding
2import Base
3import bend-kit-bytes@0.3.0.0/bytes.bend as Bytes
4
5# UTF-8 (RFC 3629). Text is a String of code points; octets are Bytes.
6# Hex lives in Bytes: Bytes.to_hex and Bytes.from_hex.
7
8# Octets being written: the buffer, the count so far, and the word being filled.
9type Out is Type:
10 Out{buf: Array<U32>, n: U32, w: U32}
11
12# Bytes enter at the top of w, as in Bytes.from_string; every fourth byte stores the word.
13def put(o: Out, +b: U32) -> Out:
14 Out{buf, +n, +w} = o
15 +w2 = ((w >> 8n) .|. (b << 24n) : U32)
16 +full = U32.is_eq((n .&. 3 : U32), 3)
17 Out{Bytes.flush(full, buf, (n >> 2n : U32), w2), (n + 1 : U32), Bool.pick(U32, full, 0, w2)}
18
19def done(o: Out) -> Bytes.Bytes:
20 Out{buf, +n, +w} = o
21 +r = (n .&. 3 : U32)
22 Bytes.Bytes{n, Bytes.flush(U32.is_ne(r, 0), buf, (n >> 2n : U32), U32.shrn(w, U32.to_nat((((4 - r) .&. 3) * 8 : U32))))}
23
24def utf8.width(+c: U32) -> U32:
25 Bool.pick(U32, U32.is_lt(c, 128), 1, Bool.pick(U32, U32.is_lt(c, 2048), 2, Bool.pick(U32, U32.is_lt(c, 65536), 3, 4)))
26
27def utf8.size(s: String, +n: U32) -> U32:
28 match s:
29 case SNil{}:
30 n
31 case SCon{Chr{+c}, t}:
32 utf8.size(t, (n + utf8.width(c) : U32))
33
34def utf8.cont(c: U32, n: Nat) -> U32:
35 (128 + U32.and(U32.shrn(c, n), 63) : U32)
36
37def utf8.push4(+c: U32, o: Out) -> Out:
38 put(put(put(put(o, (240 + U32.shrn(c, 18n) : U32)), utf8.cont(c, 12n)), utf8.cont(c, 6n)), utf8.cont(c, 0n))
39
40def utf8.push3(+c: U32, o: Out, small: Bool) -> Out:
41 match small:
42 case True{}:
43 put(put(put(o, (224 + U32.shrn(c, 12n) : U32)), utf8.cont(c, 6n)), utf8.cont(c, 0n))
44 case False{}:
45 utf8.push4(c, o)
46
47def utf8.push2(+c: U32, o: Out, small: Bool) -> Out:
48 match small:
49 case True{}:
50 put(put(o, (192 + U32.shrn(c, 6n) : U32)), utf8.cont(c, 0n))
51 case False{}:
52 utf8.push3(c, o, U32.is_lt(c, 65536))
53
54def utf8.push(+c: U32, o: Out, ascii: Bool) -> Out:
55 match ascii:
56 case True{}:
57 put(o, c)
58 case False{}:
59 utf8.push2(c, o, U32.is_lt(c, 2048))
60
61def utf8.encode.go(s: String, o: Out) -> Out:
62 match s:
63 case SNil{}:
64 o
65 case SCon{Chr{+c}, t}:
66 utf8.encode.go(t, utf8.push(c, o, U32.is_lt(c, 128)))
67
68# Text to octets. One pass sizes the buffer; a second fills it.
69def utf8.encode(+s: String) -> Bytes.Bytes:
70 done(utf8.encode.go(s, Out{Bytes.alloc(utf8.size(s, 0)), 0, 0}))
71
72# Decoder state: continuation bytes still needed, the allowed range of the
73# next one, the code point so far, and the output reversed.
74type Utf8 is Data:
75 Utf8{need: U32, lo: U32, hi: U32, cp: U32, out: String}
76
77def utf8.bad(out: String) -> String:
78 SCon{Chr{65533}, out}
79
80def utf8.lead.multi(+b: U32, out: String) -> Utf8:
81 +need = Bool.pick(U32, U32.is_lt(b, 224), 1, Bool.pick(U32, U32.is_lt(b, 240), 2, 3))
82 +lo = Bool.pick(U32, U32.is_eq(b, 224), 160, Bool.pick(U32, U32.is_eq(b, 240), 144, 128))
83 +hi = Bool.pick(U32, U32.is_eq(b, 237), 159, Bool.pick(U32, U32.is_eq(b, 244), 143, 191))
84 Utf8{need, lo, hi, U32.and(b, U32.shrn(63, U32.to_nat(need))), out}
85
86def utf8.lead.high(+b: U32, out: String, bad: Bool) -> Utf8:
87 match bad:
88 case True{}:
89 Utf8{0, 128, 191, 0, utf8.bad(out)}
90 case False{}:
91 utf8.lead.multi(b, out)
92
93# WHATWG: a byte that cannot start a sequence decodes to U+FFFD.
94def utf8.lead(+b: U32, out: String, ascii: Bool) -> Utf8:
95 match ascii:
96 case True{}:
97 Utf8{0, 128, 191, 0, SCon{Chr{b}, out}}
98 case False{}:
99 utf8.lead.high(b, out, Bool.or(U32.is_lt(b, 194), U32.is_lt(244, b)))
100
101def utf8.more(+need: U32, cp: U32, out: String, done: Bool) -> Utf8:
102 match done:
103 case True{}:
104 Utf8{0, 128, 191, 0, SCon{Chr{cp}, out}}
105 case False{}:
106 Utf8{need, 128, 191, cp, out}
107
108# WHATWG: a byte that breaks a sequence yields U+FFFD and is read again as a lead.
109def utf8.cont.in(+need: U32, cp: U32, +b: U32, out: String, ok: Bool) -> Utf8:
110 match ok:
111 case True{}:
112 utf8.more((need - 1 : U32), (cp * 64 + U32.and(b, 63) : U32), out, U32.is_eq(need, 1))
113 case False{}:
114 utf8.lead(b, utf8.bad(out), U32.is_lt(b, 128))
115
116def utf8.step.if(+need: U32, +lo: U32, +hi: U32, cp: U32, +b: U32, out: String, idle: Bool) -> Utf8:
117 match idle:
118 case True{}:
119 utf8.lead(b, out, U32.is_lt(b, 128))
120 case False{}:
121 utf8.cont.in(need, cp, b, out, Bool.and(U32.is_le(lo, b), U32.is_le(b, hi)))
122
123def utf8.step.st(st: Utf8, +b: U32) -> Utf8:
124 Utf8{+need, +lo, +hi, cp, out} = st
125 utf8.step.if(need, lo, hi, cp, b, out, U32.is_eq(need, 0))
126
127def utf8.finish(st: Utf8) -> String:
128 Utf8{+need, lo, hi, cp, +out} = st
129 String.reverse(Bool.pick(String, U32.is_eq(need, 0), out, utf8.bad(out)))
130
131def utf8.decode.go(n: Nat, r: Array<U32> & U32, +i: U32, st: Utf8) -> String:
132 match n:
133 case 0n:
134 (a, v) = r
135 utf8.finish(st)
136 case 1n+p:
137 (a, +b) = r
138 utf8.decode.go(p, Bytes.peek(a, (i + 1 : U32)), (i + 1 : U32), utf8.step.st(st, b))
139
140# Octets to text, as WHATWG decodes UTF-8: malformed input becomes U+FFFD.
141def utf8.decode(b: Bytes.Bytes) -> String:
142 Bytes.Bytes{+len, buf} = b
143 utf8.decode.go(U32.to_nat(len), Bytes.peek(buf, 0), 0, Utf8{0, 128, 191, 0, SNil{}})