Lowent 매뉴얼←↑→

32 표준 라이브러리의 지도

먼저 알아야 할 것

21장 모듈 · 표준 라이브러리 이름은 use <이름> . 으로 들여온다
16장 권한 · 권한은 건네받는 것이지 주워 쓰는 것이 아니다
17장 실패를 설계하기 · 경계에서는 실패를 값으로 돌려준다

돌아보기

21장에서 use 가 찾는 것은 파일 이름인가, 모듈 이름인가? 표준 라이브러리에서 이 차이가 왜 걸린다고 했는가?

답. 파일 안의 module 선언이다. 표준 라이브러리에는 파일 이름과 모듈 이름이 다른 곳이 몇 있어서(lib/alloc.low 는 allocs, lib/str.low 는 strings), 파일 이름으로 들여오려다 헤맨다. 이 장의 첫 표가 그 목록이다.

이 장의 필요성과 맥락

제9부는 표준 라이브러리를 둘러본다. 모듈마다 자세한 설명은 부록 E 에 한 쪽씩 있고, 이 부는 그것을 되풀이하지 않는다. 대신 어떤 모듈이 어떤 층에 있고, 모두가 어떤 규약을 따르며, 무엇을 할 때 어디를 보면 되는지를 정리한다. 그 첫 장인 이 장은 지도다 — 언어와 라이브러리의 경계, 라이브러리가 들어오는 기준, 모듈의 층과 공통 규약.

이 장이 끝나면

언어·잎(leaf)·라이브러리 세 층을 가르는 물음을 알게 된다. 표준 라이브러리가 들어오는 기준(헌장)과 성숙도 사다리, 모듈을 할당하지 않는 층·저장이 필요한 층·권한이 필요한 층으로 나눈 지도를 익힌다. 호출자의 버퍼, 값으로 돌아오는 실패, 전량 아니면 무, 갚아야 하는 소유라는 공통 규약과, 파일 이름과 모듈 이름이 다른 곳들도 보게 된다.

이 장에서 답할 질문

  1. 언어에 넣으면 편할 기능을 왜 라이브러리로 두는가?

32.1 세 층 — 언어, 잎, 라이브러리#

층누가 만드나자격
언어명세라이브러리로 표현할 수 없거나, 표현하면 비용이 숨는다
잎(leaf)처리기운영체제·하드웨어에 닿는다. Lowent 로 쓸 수 없다
라이브러리누구나나머지 전부. Lowent 로 쓴다

표 32.1 — 세 층

어떤 조각이 잎인지를 가르는 물음은 하나다 — “이것을 Lowent 로 쓸 수 있는가.” 쓸 수 있으면 라이브러리다. 파일을 열어 정수 핸들을 받는 것은 커널에 닿으므로 잎이다. 그 핸들을 잊을 수 없는 소유 값으로 감싸는 것은 Lowent 로 쓸 수 있으므로 라이브러리다. 파일을 통째로 읽어 주는 편의 op 도 라이브러리다. 편의는 잎의 자격이 되지 않는다.

표준 라이브러리는 특권이 없다. 저자가 쓴 코드와 같은 규칙 — 계약, 효과, 권한 — 을 받는다. strings.starts_with 의 소스는 이렇다.

export fn starts_with input s str . input prefix str . output bool . do
  guard le (len prefix) (len s) . else return false .
  return eq_str (subslice s 0 (len prefix)) prefix .
end

이 책에서 배운 것만으로 읽힌다. 라이브러리만 쓸 수 있는 숨은 통로가 있으면 그 통로를 지나는 순간 프로그램의 보장이 끊기기 때문이다.

문. 언어에 넣으면 편할 기능을 왜 라이브러리로 두는가?

답. 언어에 낱말이나 내장 연산을 더하면 모든 프로그램이 그것을 외워야 하고, 처리기가 그것을 특별하게 다뤄야 한다. 라이브러리로 두면 같은 규칙으로 검사되고, 쓰지 않는 프로그램에는 없는 것이다. 그래서 언어는 표현할 수 없는 것만 받아들인다. 표준 라이브러리의 할당기·그릇·파일 핸들이 모두 이 기준을 통과해 라이브러리에 있다. 편하다는 것은 언어에 들어갈 이유가 되지 않는다.

32.2 무엇이 들어오는가#

라이브러리에 들어오는 것은 있어서가 아니라 증거로 들어온다. 명세의 헌장은 여섯 잣대를 둔다.

잣대뜻
이 언어로 쓸 수 있으면 이 언어로 쓴다잎은 바깥 권위를 쓰거나 이 언어가 표현하지 못할 때뿐이다
시그니처가 비용과 권한의 장부다효과·권한·저장·할당·소유·완결이 경계에서 보여야 한다
셈과 권위를 가른다꼴 짓기와 내보내기, 해석과 파일 열기, 난수의 다음 걸음과 엔트로피 얻기는 다른 것이다
자리는 여러 축으로 가른다권위·저장·실행 등급·프로파일을 한 사다리로 눌러 담지 않는다
안에 있는 것은 작게 둔다이식되는 바탕과 얇은 호스트 어댑터까지
모듈마다 증거가 있다단위 시험·거절 시험·대조·실패 시험을 갖춘다

표 32.2 — 라이브러리 헌장

모듈은 저마다 성숙도를 적는다 — experimental(적혀 있을 뿐) · incubating(결정 문서가 있다) · standard(설명이 있고 회귀 시험이 이름으로 부른다) · deprecated(무엇으로 갈아탈지 적혀 있다). 사다리의 값은 위 칸이 아니라 아래 칸에 있다. experimental 이라고 적는 것은 “아직 믿지 말라” 를 소스에 적는 일이다. 칸은 품질의 등수가 아니라 약속의 크기다.

명세는 라이브러리의 목록을 적지 않는다. 무엇이 있는지는 소스에서 매번 다시 세어 내는 쪽이 권위이고, 문서가 그것을 베끼면 베낀 순간부터 갈라진다. 실제로 있지도 않은 API 셋이 표에 있다는 이유로 사실로 읽힌 일이 있었다. 이 부의 표도 이 판의 소스를 보고 만든 것이며, 모듈별 자세한 쪽은 부록 E 에 있다.

32.3 층으로 본 지도#

층성격대표 모듈
L0 순수 계산effects none · 호출자의 버퍼strings strbuf fmt utf8 utf16 unicode codec regex hash math random sortlib sortgen searchlib hashmap strmap spsc term · 암호 모듈들
L1 저장할당 권한이나 빌린 바이트가 필요하다allocs pool shard budget segarena pagecache growvec vecgen mapgen nodelist segview wire flags lifemode
L2 호스트권한이 필요하다(cap io·file_system·tty·net·clock)io outbuf files tty net clock

표 32.3 — 표준 라이브러리의 층

이 부의 나머지 장은 쓰임새로 묶는다. 글자와 부호화(33장), 그릇과 정렬(34장), 저장소와 핸들 (35장), 입출력과 네트워크와 암호(36장), 터미널(37장)이다.

32.4 파일 이름과 모듈 이름#

use 가 찾는 것은 파일 안의 module 선언이다. 몇 곳은 파일 이름과 다르다.

소스 파일모듈 이름
lib/alloc.lowallocs
lib/str.lowstrings
lib/vec.lowvecs
lib/sort.lowsortlib
lib/search.lowsearchlib
lib/out.lowoutbuf
lib/file.lowfiles

표 32.4 — 파일 이름과 다른 모듈 이름

examples/ch32/names.low

module names .
rem run: is_get [71,69,84,32,47]
rem run: is_get [80,79,83,84]

use strings .

fn is_get input line slice u8 . output bool .
do
  return strings.starts_with line "GET " .
end

실행 결과

$ lowentc --run is_get names.low [71,69,84,32,47]
is_get([71,69,84,32,47]) = 1
  arg0 (written) = [71,69,84,32,47]
$ lowentc --run is_get names.low [80,79,83,84]
is_get([80,79,83,84]) = 0
  arg0 (written) = [80,79,83,84]

use strings . 은 from 없이 표준 모듈 자리에서 풀린다. 들여온 이름은 언제나 strings.starts_with 처럼 모듈로 한정해 부른다. 글롭 들여오기는 없다. 파일 이름으로 들여오면 그런 모듈이 없다.

examples/ch32/wrong_name.low

module wrong_name .
rem expect: W-USE-EXTERNAL

use str .

fn is_get input line slice u8 . output bool .
do
  return true .
end

실행 결과

$ lowentc --check wrong_name.low
wrong_name.low:4:0 W-USE-EXTERNAL: this module is not in the compilation unit — there is no module search path, so nothing here can confirm it exists. Pass the file that declares it and it WILL be checked (several .low files link into one unit)

진단이 오류가 아니라 경고인 것은 검색 경로가 없어서 이 번역 단위만 보고는 그 이름이 어딘가에 있는지 없는지 알 수 없기 때문이다 (21장). 그 이름을 실제로 쓰는 순간 거절된다.

32.5 모든 모듈이 따르는 규약#

규약뜻
버퍼는 호출자의 것L0 모듈은 할당하지 않는다. 출력 버퍼와 작업 공간을 매개변수로 받는다. 그래서 재진입이 공짜고, VM·네이티브 대조가 그대로 돈다
실패는 값option(못 했다)이나 result(무엇이 실패했는지). 트랩도, 조용한 잘림도, 치환 문자도 없다
전량 아니면 무자리가 모자라면 한 바이트도 쓰지 않는다. 절반 쓴 버퍼는 조용히 틀린 출력이다
소유는 갚아야 한다완결이 필요한 것(outbuf 의 남은 바이트, files 의 핸들)은 소유 값이라 잊으면 번역이 거절한다

표 32.5 — 표준 라이브러리의 공통 규약

흔한 오해. 표준 라이브러리니까 모든 모듈을 똑같이 믿어도 된다

모듈마다 성숙도가 다르고, 모듈 문서의 첫머리에 무엇을 하지 않는지가 적혀 있다. 예를 들어 tlssrv 는 핸드셰이크를 모두 하지만 전송은 아직이고, der 은 공개키만 꺼내는 최소 파서이지 인증서 체계(PKI)가 아니다. 들여오기 전에 그 모듈이 스스로 적은 경계를 읽는 것이 이 라이브러리를 쓰는 법이다.

32.6 잎 op 의 목록 — 모듈 아래에 무엇이 있나#

라이브러리 모듈은 결국 처리기가 주는 잎 내장 op 을 부른다. 보통은 모듈을 쓰고 잎을 직접 부르지 않는다 — 모듈이 소유·세 자리 답·전량 아니면 무 같은 규약을 입혀 주기 때문이다. 그래도 어느 모듈이 무엇을 딛는지 알면 모듈 문서를 읽거나 새 모듈을 지을 때 길을 잃지 않는다.

잎 op하는 일감싸는 모듈 · 권한
file_open · file_read · file_write · file_seek · file_close파일을 열고 읽고 쓰고 자리를 옮기고 닫는다files · cap file_system
dir_make · dir_read · dir_close디렉터리를 만들고 항목을 읽고 닫는다files · cap file_system
path_remove · path_rename · link_type경로를 지우고 이름을 바꾸고, 심링크를 따라가지 않고 그 자신이 무엇인지 묻는다files · cap file_system
net_listen · net_accept · net_connect · net_port듣는 소켓 · 연결 받기 · 주소와 포트로 연결 걸기 · 포트 묻기net · cap net
net_resolve이름을 IPv4 주소로 — DNS. 연결과 가른 잎이라, 주소를 이미 가진 프로그램은 이름 해석에 닿지 않는다net · cap net
net_send · net_recv · net_close · net_pair보내기 · 받기 · 닫기 · 맞물린 한 쌍net · cap net
reactor_new · r_read · r_writereactor 를 만든다 · reactor 로 읽는다 · reactor 로 쓴다모듈 없음 — cap io
env_get환경 변수를 읽는다모듈 없음 — cap env
rng_next상태에서 다음 난수(splitmix64)random — 권한 없음(순수)
hash_bytes · crc32FNV-1a 64 해시 · CRC-32 검사값hash — 권한 없음
sha256 · sha384 · sha512SHA-256 · SHA-384 · SHA-512 — SHA-384 은 SHA-512 의 다른 시작값이고 앞 48 바이트다hash·hmac·tls13 · ed25519 — 권한 없음
aes_ctr · ghashAES-128-CTR 흐름 · GHASH 누산 — 키·카운터·누산기가 모두 16 바이트다. 카운터와 누산기는 제자리에서 고쳐지므로 이어 불러도 이어진다gcm — 권한 없음
chacha_polyChaCha20-Poly1305 한 덩이 — 흐름과 누산을 한 바퀴에. 뜻은 chacha20 을 돌리고 그 암호문을 poly1305 에 먹인 것과 같다. 두 셈이 쓰는 자리가 달라(벡터 · 정수 곱셈기) 섞으면 겹쳐 돈다(실측 1,080 → 1,199 MB/s)aead — 권한 없음
aes_gcmAES-128-GCM 한 덩이 — 흐름과 누산을 한 바퀴에 돈다. 뜻은 aes_ctr 를 돌리고 그 암호문을 ghash 에 먹인 것과 같다; 다른 것은 빠르기뿐이다(실측 2848 → 3661 MB/s). 봉인만 쓴다 — 복호는 태그를 먼저 증언하고 맞을 때만 풀어야 하므로 두 바퀴로 남는다gcm — 권한 없음
chacha20 · poly1305ChaCha20 흐름 · Poly1305 누산 — 키 32 · 카운터 블록 16(앞 4 가 블록 번호, 리틀엔디언)이고 카운터는 제자리에서 올라간다. poly1305 은 상태(h 다섯 · r 다섯)를 누산하고, 열여섯의 배수가 아니면 꼬리를 0 으로 채운다. 폭은 빌드가 정한다(lowentc --hw sse2 · avx2)chacha · poly · aead — 권한 없음
aes_round · aes_round_lastAES 한 라운드 — 상태 16 바이트를 제자리에서. 기계에 그 명령이 있으면 처리기가 그것으로 내리고(lowentc --hw aes), 없으면 같은 답을 내는 표 셈으로 내린다aes — 권한 없음
str_from_cstrC 문자열(NUL 로 끝남)을 훑어 str 로 만든다C 경계 — VM 은 E-VM-CSTR 로 못 한다고 말한다

표 32.6 — 잎 내장 op 과 그것을 감싸는 모듈

계산 잎은 call_builtin 뒤에서만 선다. call_builtin sha256 msg out 처럼 이름이 그 자리에만 살고 전역 어휘가 되지 않는다 — 한 번 쓰는 낱말로 모두가 외울 것을 늘리지 않으려는 규율이고, pipe 안의 단계 이름과 cast u8 x 의 타입 자리가 이미 같은 방식이다. 운영체제에 닿는 잎(file_open · net_send …)은 그대로 부른다 — 첫 피연산자가 권한이라 이미 특별함이 보인다.

잎의 자격은 “Lowent 로 쓸 수 있는가” 하나다. rng_next 와 해시 셋이 순수 계산인데도 잎인 것은, 알고리즘을 처리기가 고정해 VM 과 네이티브가 비트까지 같은 답을 내게 하려는 것이다. 권한이 필요한 잎은 모두 그 권한을 첫 피연산자로 받는다(16장).

32.7 흔한 실수#

반례. option 으로 돌아오는 쓰기를 묻지 않고 이어 쓴다

examples/ch32/mistake_chainnone.low

module mistake_chainnone .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem trap: greet [0,0,0,0,0,0]

use fmt .

proc greet input buf mut slice u8 . output u64 . effects none .
do
  rem ✘ 앞의 쓰기가 성공했다고 믿고 `some_value` 로 곧장 이어 쓴다
  let p u64 be some_value (fmt.put_str buf 0 "hello") .
  let q u64 be some_value (fmt.put_byte buf p 32) .
  return some_value (fmt.put_u64 buf q 42) .
end

실행 결과

$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
  arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet mistake_chainnone.low [0,0,0,0,0,0]
== ir diagnostics (1) ==
0:0 E-VM-NONE: some_value of none (panic)

fmt 의 쓰기 op 은 다음 쓸 자리를 option u64 로 준다. 열두 칸 버퍼에서는 셋 다 성공해 8 을 내지만, 여섯 칸에서는 put_u64 가 자리가 모자라 none 을 주고 some_value 가 멈춘다. L0 모듈의 실패는 트랩이 아니라 값이므로, 받는 쪽이 걸음마다 묻는다.

examples/ch32/chainnone_fixed.low

module chainnone_fixed .
rem run: greet [0,0,0,0,0,0,0,0,0,0,0,0]
rem run: greet [0,0,0,0,0,0]

use fmt .

proc greet input buf mut slice u8 . output u64 . effects none .
do
  rem 걸음마다 묻는다 --- 모자라면 0 을 돌려 "쓰지 못했다" 를 알린다
  let p option u64 be fmt.put_str buf 0 "hello" .
  guard is_some p . else return 0 .
  let q option u64 be fmt.put_byte buf (some_value p) 32 .
  guard is_some q . else return 0 .
  let r option u64 be fmt.put_u64 buf (some_value q) 42 .
  guard is_some r . else return 0 .
  return some_value r .
end

실행 결과

$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0,0,0,0,0,0,0]
greet([104,101,108,108,111,32,52,50,0,0,0,0]) = 8
  arg0 (written) = [104,101,108,108,111,32,52,50,0,0,0,0]
$ lowentc --run greet chainnone_fixed.low [0,0,0,0,0,0]
greet([104,101,108,108,111,32]) = 0
  arg0 (written) = [104,101,108,108,111,32]

고친 판은 여섯 칸 버퍼에서 0 을 돌려준다. 버퍼에 hello 가 남아 있는 것에 주목한다. “전량 아니면 무” 는 op 한 번의 약속이지 여러 번 부른 묶음 전체의 약속이 아니다. 묶음 전체를 되돌려야 하면 성공한 자리(p·q)까지를 유효한 길이로 삼는다.

흔한 오해. 버퍼가 모자라면 들어가는 만큼은 쓴다

examples/ch32/all_or_nothing.low

module all_or_nothing .
rem run: try_put [0,0,0,0]

use fmt .

proc try_put input buf mut slice u8 . output u64 . effects none .
do
  rem 여섯 바이트를 네 칸에 --- 앞 네 바이트만 쓰는 대신 하나도 쓰지 않는다
  let p option u64 be fmt.put_str buf 0 "Lowent" .
  guard is_some p . else return 99 .
  return some_value p .
end

실행 결과

$ lowentc --run try_put all_or_nothing.low [0,0,0,0]
try_put([0,0,0,0]) = 99
  arg0 (written) = [0,0,0,0]

C 의 snprintf 는 들어가는 만큼 쓰고 잘린다. 표준 라이브러리의 L0 모듈은 전량 아니면 무다. 여섯 바이트를 네 칸에 쓰려 하면 한 바이트도 쓰지 않고 none 을 주며, 결과의 버퍼는 [0,0,0,0] 그대로다. 절반 쓴 버퍼는 조용히 틀린 출력이 되고, 부르는 쪽은 “none 이면 버퍼는 그대로” 를 믿고 다른 길로 이어 쓸 수 있다.

32.8 이 장의 문법 한눈에#

모양뜻왜 이렇게
use strings .표준 모듈을 들여온다 — 이름은 파일 안의 module 선언lib/str.low 의 모듈은 strings
strings.starts_with line "GET "들여온 이름은 모듈로 한정한다글롭 들여오기가 없다
fmt.put_str buf pos s → option u64호출자의 버퍼에 쓰고 다음 자리를 준다L0 은 할당하지 않는다 — 재진입이 공짜
none(자리가 모자람)한 바이트도 쓰지 않았다전량 아니면 무 — op 한 번의 약속
result t e무엇이 실패했는지 말하는 실패트랩도 조용한 잘림도 없다
owned 핸들 · 남은 바이트갚아야 하는 소유잊으면 번역이 거절한다
experimental · incubating · standard · deprecated모듈이 스스로 적는 성숙도칸은 등수가 아니라 약속의 크기

표 32.7 — 표준 라이브러리를 쓰는 모양 — 모양 · 뜻 · 왜 이렇게 생겼나

복습 정리

언어는 표현할 수 없는 것만, 잎은 바깥에 닿는 얇은 조각만 맡고, 나머지는 같은 규칙을 받는 라이브러리다. 라이브러리는 헌장의 증거로 들어오고 성숙도를 소스에 적는다. 모듈은 순수 계산(L0)·저장(L1)·호스트(L2) 층으로 나뉜다. 모듈 이름은 파일 이름과 다를 수 있다. 모든 모듈은 호출자의 버퍼·값으로 돌아오는 실패·전량 아니면 무·갚아야 하는 소유를 따른다.