Lowent 매뉴얼←↑→

utf16 — UTF-16 서로게이트 산술

소스
lib/utf16.low
층
L0 — 순수 계산
권한
없음

UTF-16 의 서로게이트 쌍을 코드포인트로 바꾸고 되돌린다. Windows API·Java·JavaScript 처럼 문자열을 16 비트 칸(unit)으로 드는 세계와 값을 주고받을 때 쓴다. slice u16 이 곧 UTF-16 문자열이고, 한 원소가 한 칸이다.

use utf16 .

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

0 은 “첫 글자” 가 아니라 0 번 칸이다. 이 모듈의 위치는 모두 칸 번호이고, 글자 단위 전진은 next 가 한다(짝이면 두 칸을 뛴다).

왜 서로게이트가 생겼는가. UTF-16 은 처음에 “문자 하나 = 16 비트 한 칸” 으로 모든 글자를 담으려 했다. 유니코드가 65,536 자를 넘어 자라자 값 구간 U+D800 … U+DFFF 를 문자로 쓰지 않기로 비워 두고, 그 구간의 두 칸을 조합해 큰 코드포인트를 적기로 했다. 상위 1024 가지 × 하위 1024 가지 = 1,048,576 가지로 U+10000 … U+10FFFF 를 딱 덮는다. 그래서 UTF-16 에서는 칸 하나를 읽는 것과 문자 하나를 읽는 것이 다르다.

왜 라이브러리인가. slice u16·slice u32 가 읽기·쓰기·색인을 이미 모두 한다. 권한도 필요 없고 언어에 없는 표현도 필요 없다. 그래서 새 타입도 새 낱말도 내장 연산도 없이 라이브러리가 되었다. 남는 진짜 일은 서로게이트 산술과 그 실패 방식뿐이다. UTF-32 라이브러리는 없다 — slice u32 의 한 칸이 곧 코드포인트라 필요한 것은 검증 한 줄(cp_valid)이다.

설계와 경계#

BMP(U+0000 … U+FFFF, 서로게이트 구간 제외)의 코드포인트는 한 칸, 값 그대로다. U+10000 … U+10FFFF 는 두 칸이다 — v = c − 0x10000 으로 20 비트를 만들고 상위 칸 hi = 0xD800 + v/1024, 하위 칸 lo = 0xDC00 + v%1024 로 가른다. 되돌리는 산술은 c = 0x10000 + (hi − 0xD800) × 1024 + (lo − 0xDC00) 이다.

칸 값(10 진)정체뜻
55296 … 56319상위 서로게이트두 칸짜리 문자의 앞 조각 — 뒤에 하위가 와야 한다
56320 … 57343하위 서로게이트두 칸짜리 문자의 뒤 조각 — 혼자 오면 틀렸다
그 밖평범한 BMP 칸그 값이 곧 코드포인트다

표 50.1 — 칸 하나의 정체

코드포인트 상한은 1114111(U+10FFFF)이다. 서로게이트 값 자체는 코드포인트가 아니고 cp_valid 가 거절한다.

op 한눈에#

op시그니처안 될 때
is_highfn (u u64) → bool실패 없음
is_lowfn (u u64) → bool실패 없음
cp_validfn (c u64) → bool실패 없음(거짓이 곧 답)
unitsfn (c u64) → u64실패 없음(1 또는 2)
unit_hifn (c u64) → option u64유효하지 않은 코드포인트면 none
unit_lofn (c u64) → option u64무효 코드포인트나 BMP(칸 하나)면 none
putproc (dst mut slice u16, at u64, c u64) → option u64무효 코드포인트나 자리 부족이면 none
decodefn (s slice u16, at u64) → option u64범위 밖이거나 짝이 맞지 않으면 none
nextfn (s slice u16, at u64) → option u64끝이거나 망가진 자리면 none
count_charsfn (s slice u16) → option u64망가진 자리를 만나면 none
is_validfn (s slice u16) → bool실패 없음

표 50.2 — utf16 의 op

실무에서 가장 많이 쓰는 것은 put(쓰기)과 decode + next(순회)다.

op 상세#

쓰는 법#

쓰기는 put 을 부르고 돌려받은 다음 위치에서 이어 쓴다. 읽기는 decode 로 값을 읽고 next 로 전진한다. 어느 쪽이든 커서는 호출자가 든다.

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 라서 한 칸. put 은 다음에 쓸 위치를 돌려준다
  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 --- BMP 밖이라 두 칸(서로게이트 짝)으로 갈라진다
  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 다시 하나로 읽는다 --- decode 가 짝을 합친다
  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' + 이모지(짝) + '가' = 칸 넷, 문자 셋
  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

상위 칸 buf[1] 은 55357(0xD83D), 하위 칸 buf[2] 는 56489(0xDCA9)다. 순회는 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

BMP 면 +1, 짝이면 +2 — 직접 세지 않는다. none 이면 멈춘다(끝인지 망가짐인지는 먼저 is_valid 를 재면 갈린다).

반례#

반례. 짝이 맞지 않는 입력을 decode 로 통과시키려 한다

set (index buf 0) 55357 .          rem ✗ 상위 0xD83D 뒤에
set (index buf 1) 65 .             rem   하위가 아니라 'A' 가 온다
let a option u64 . be u.decode buf 0 .

none 이다. 하위 서로게이트가 혼자 오거나(decode buf 2, 값 56489), 상위 서로게이트로 끝나도(decode (subslice buf 0 4) 3) 마찬가지다. 셋 다 guard is_some 자리에서 바로 걸린다. 조용히 통과시키면 그 뒤가 모두 틀리므로 거절이 곧 이 라이브러리의 값이다. 온전한 짝은 여전히 통과한다.

반례. 두 칸짜리를 마지막 한 칸에 넣으려 한다

u.put buf (sub (len buf) 1) 128169 는 none 이고 그 칸은 더럽혀지지 않는다. 반쯤 쓰다 말았나 걱정하지 않아도 된다.

반례. 서로게이트 값을 코드포인트로 쓰려 한다

u.cp_valid 55357 은 false 이고, 그래서 put·unit_hi·unit_lo 가 모두 none 이다. 이 none 들을 검사 없이 some_value 로 꺼내면 실행 중 E-VM-NONE 으로 멈춘다 — 번역은 통과하므로 guard 가 먼저다.

주의#