Lowent Manual←↑→

utf16 — UTF-16 surrogate arithmetic

Source
lib/utf16.low
Layer
L0 — pure computation
Capabilities
none

Turns UTF-16 surrogate pairs into code points and back. Use it when exchanging values with worlds that hold strings as 16-bit units, such as the Windows API, Java and JavaScript. slice u16 is a UTF-16 string, and one element is one unit.

use utf16 .

let c option u64 . be utf16.decode s 0 .
guard is_some c . else return 1 .

0 is not “the first character” but unit 0. Every position in this module is a unit index, and advancing by character is done by next (two units for a pair).

Why surrogates exist. UTF-16 first tried to hold every character as “one character = one 16-bit unit”. When Unicode grew beyond 65,536 characters, the value range U+D800 … U+DFFF was left unused as characters, and two units from that range were combined to write large code points. 1024 high values × 1024 low values = 1,048,576, covering exactly U+10000 … U+10FFFF. So in UTF-16 reading one unit and reading one character differ.

Why it is a library. slice u16 and slice u32 already do reading, writing and indexing. No capability and no expression missing from the language is needed. So it became a library with no new type, word or builtin. The only real work left is surrogate arithmetic and how it fails. There is no UTF-32 library — one unit of slice u32 is a code point, so all that is needed is one line of validation (cp_valid).

Design and boundaries#

A BMP code point (U+0000 … U+FFFF excluding the surrogate range) is one unit, value as is. U+10000 … U+10FFFF is two units — make 20 bits with v = c − 0x10000 and split into the high unit hi = 0xD800 + v/1024 and the low unit lo = 0xDC00 + v%1024. The reverse arithmetic is c = 0x10000 + (hi − 0xD800) × 1024 + (lo − 0xDC00).

Unit value (decimal)What it isMeaning
55296 … 56319High surrogateThe front piece of a two-unit character — a low must follow
56320 … 57343Low surrogateThe back piece of a two-unit character — wrong if alone
OtherwiseAn ordinary BMP unitIts value is the code point

Table 50.1 — What a single unit is

The code point upper limit is 1114111 (U+10FFFF). Surrogate values themselves are not code points, and cp_valid rejects them.

Ops at a glance#

opSignatureWhen it cannot
is_highfn (u u64) → boolnever fails
is_lowfn (u u64) → boolnever fails
cp_validfn (c u64) → boolnever fails (false is the answer)
unitsfn (c u64) → u64never fails (1 or 2)
unit_hifn (c u64) → option u64none for an invalid code point
unit_lofn (c u64) → option u64none for an invalid code point or BMP (one unit)
putproc (dst mut slice u16, at u64, c u64) → option u64none for an invalid code point or no room
decodefn (s slice u16, at u64) → option u64none if out of range or unpaired
nextfn (s slice u16, at u64) → option u64none at the end or at a broken place
count_charsfn (s slice u16) → option u64none on meeting a broken place
is_validfn (s slice u16) → boolnever fails

Table 50.2 — Ops of utf16

The most used in practice are put (writing) and decode + next (iteration).

Ops in detail#

Using it#

Writing calls put and continues from the returned next position. Reading reads values with decode and advances with next. Either way the caller holds the cursor.

module ex_utf16 .

use utf16 as u .

proc round_trip input buf mut slice u16 . . output u64 . effects none . do
  guard ge (len buf) 4 . else return 90 .
  rem '가' (U+AC00) --- BMP, so one unit. put returns the next position to write
  let a option u64 . be u.put buf 0 44032 .
  guard is_some a . else return 1 .
  guard eq (some_value a) 1 . else return 2 .

  rem U+1F4A9 --- outside the BMP, so split into two units (a surrogate pair)
  let b option u64 . be u.put buf (some_value a) 128169 .
  guard is_some b . else return 3 .
  guard eq (some_value b) 3 . else return 4 .
  guard eq (index buf 1) 55357 . else return 5 .
  guard eq (index buf 2) 56489 . else return 6 .

  rem read it back as one --- decode joins the pair
  let rb option u64 . be u.decode buf 1 .
  guard is_some rb . else return 7 .
  guard eq (some_value rb) 128169 . else return 8 .

  rem 'A' + emoji (pair) + '가' = four units, three characters
  set (index buf 0) 65 .
  set (index buf 3) 44032 .
  let n option u64 . be u.count_chars (subslice buf 0 4) .
  guard is_some n . else return 9 .
  guard eq (some_value n) 3 . else return 10 .
  return 42 .
end

The high unit buf[1] is 55357 (0xD83D) and the low unit buf[2] is 56489 (0xDCA9). Iterate with next.

var i u64 be 0 .
while lt i (len s) . do
  let c option u64 . be u.decode s i .
  guard is_some c . else return 80 .
  let nx option u64 . be u.next s i .
  guard is_some nx . else return 81 .
  set i (some_value nx) .
end

+1 for BMP, +2 for a pair — you do not count. none means stop (whether it is the end or breakage is told apart by measuring is_valid first).

Counter-examples#

Counter-example. Trying to pass unpaired input through decode

set (index buf 0) 55357 .          rem ✗ after the high 0xD83D
set (index buf 1) 65 .             rem   comes 'A', not a low
let a option u64 . be u.decode buf 0 .

It is none. So is a low surrogate alone (decode buf 2, value 56489) or ending with a high surrogate (decode (subslice buf 0 4) 3). All three are caught right at guard is_some. Letting them through silently would make everything after wrong, so rejection is the value of this library. Intact pairs still pass.

Counter-example. Putting a two-unit character into the last unit

u.put buf (sub (len buf) 1) 128169 is none and that unit is not touched. No need to worry about half a write.

Counter-example. Trying to use a surrogate value as a code point

u.cp_valid 55357 is false, so put, unit_hi and unit_lo all give none. Taking these nones out with some_value without checking stops at run time with E-VM-NONE — it translates, so guard comes first.

Cautions#