Lowent 매뉴얼←↑→

13 이름 붙인 타입 — type·newtype·range·cast

먼저 알아야 할 것

4장 수 · 넓히기는 자동, 좁히기는 적는다
10장 묶음 · struct 와 field

돌아보기

4장에서 usize 와 u64 는 폭이 같은 기계에서도 서로 자동으로 바뀌지 않는다고 했다. 왜 그렇게 정했는가?

답. 주소를 세는 수와 그냥 수를 섞는 자리를 소스에 드러내기 위해서다. 표현이 같아도 뜻이 다르면 다른 타입으로 두고, 건너는 자리를 적게 한다. 이 장은 같은 원칙을 사용자가 직접 쓰는 방법 — newtype — 과, 타입에 범위와 폭과 배치를 적는 방법을 다룬다.

이 장의 필요성과 맥락

사용자 번호와 주문 번호가 둘 다 u64 이면 바꿔 넣어도 컴파일러는 모른다. 퍼센트를 받는 매개변수에 300 을 넘겨도 모른다. 이런 결함은 타입이 표현만 말하고 뜻을 말하지 않아서 생긴다. 제3부의 마지막 장으로, 앞에서 배운 타입들에 이름과 범위와 배치를 붙여 뜻을 싣는 법을 모은다. 계약(제4부)으로 넘어가기 직전에 두는 것은 range 가 계약과 같은 자격으로 쓰이기 때문이다.

이 장이 끝나면

type 은 같은 타입의 다른 이름이고 newtype 은 같은 표현의 다른 타입이라는 것, 둘 사이를 cast 로 건넌다는 것을 알게 된다. 매개변수 자리의 range lo hi 가 부르는 쪽과 본문에 무엇을 주는지, cast 가 widen·narrow 와 어떻게 다른지를 익힌다. 1 부터 64 비트까지의 bits 정수와, 바이트 배치를 못 박는 layout packed 와 view 도 보게 된다.

이 장에서 답할 질문

  1. newtype 을 쓰면 실행 비용이 드는가?

13.1 type 은 별칭, newtype 은 새 타입#

type <이름> <타입> . 은 기존 타입에 다른 이름을 준다. 두 이름은 같은 타입이고 서로 바꿔 쓸 수 있다.

examples/ch13/aliases.low

module aliases .
rem run: area 3 4
rem run: half_percent 90

type meters u64 .
type pct range 0 100 .

fn area input w meters . input h meters . output u64 .
  requires le w. 1000000 . .
  requires le h. 1000000 . .
do
  return mul w. h. . .
end .

fn half_percent input p pct . output u8 .
do
  return narrow u8 div p. 2 . . .
end .

실행 결과

$ lowentc --run area aliases.low 3 4
area(3, 4) = 12
$ lowentc --run half_percent aliases.low 90
half_percent(90) = 45

meters 는 u64 의 다른 이름일 뿐이다. 그래서 mul w. h. . 의 결과를 u64 로 돌려줄 수 있다. 별칭은 긴 타입을 짧게 부르거나(type found option u64 .), 뜻을 소스에 남길 때 쓴다. pct 는 범위가 붙은 타입의 별칭이고, 이 이름을 받는 모든 자리가 그 범위를 물려받는다(아래에서 다룬다).

newtype <이름> <타입> . 은 기존 타입과 같은 표현을 갖되 다른 타입을 만든다.

examples/ch13/ids.low

module ids .
rem expect: E-TYPE-NOMINAL

newtype user_id u64 .
newtype order_id u64 .

fn load_order input id order_id . output u64 .
do
  return 0 .
end .

fn mistake input u user_id . output usize .
do
  return load_order u. . .
end .

실행 결과

$ lowentc --check ids.low
ids.low:14:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)
ids.low:14:0 E-TYPE-NOMINAL: these are NOMINALLY DISTINCT types with the same representation — a `newtype` (or usize/idiff) is not interchangeable with its base or with another newtype. Convert explicitly with `cast <type> <value>` (RFC-0002 §6.3.1)

user_id 와 order_id 는 둘 다 64 비트 수로 표현되지만 서로 바꿔 넣을 수 없다. 이런 실수는 테스트로 찾기 어렵다 — 번호가 우연히 겹치면 엉뚱한 주문을 불러오고도 조용하다. newtype 으로 갈라 두면 번역이 잡는다.

 type meters u64 .                newtype user_id u64 .

   meters ──┐                     user_id ◀── cast ──▶ u64
            ├──▶ u64
   u64 ─────┘                     표현은 같고 타입은 둘
   이름은 둘, 타입은 하나         바꿔 넣으면 E-TYPE-NOMINAL
   섞어도 아무도 막지 않는다      건너는 자리는 cast 로 드러난다

일부러 건너야 할 때는 cast 로 적는다.

examples/ch13/ids_ok.low

module ids_ok .
rem run: roundtrip 42

newtype user_id u64 .

fn user_of input n u64 . output user_id .
do
  return cast user_id n. . .
end .

fn raw_of input u user_id . output u64 .
do
  return cast u64 u. . .
end .

fn roundtrip input n u64 . output u64 .
do
  return raw_of user_of n. . . .
end .

실행 결과

$ lowentc --run roundtrip ids_ok.low 42
roundtrip(42) = 42

user_of 와 raw_of 처럼 건너는 op 을 한두 개만 두면, 날것의 수가 user_id 로 들어오는 자리가 코드에서 그 둘로 모인다. 그 자리에서만 검사하면 된다.

문. newtype 을 쓰면 실행 비용이 드는가?

답. 들지 않는다. 표현이 같으므로 네이티브 코드에서 user_id 는 그냥 64 비트 정수다. 구별은 번역할 때만 있고, cast user_id n. . 은 값을 바꾸지 않는다. 비용 없는 구별이라서 적극적으로 쓸 만하다.

13.2 range — 매개변수의 생김새가 된 계약#

매개변수의 타입 자리에 range <아래> <위> 를 적으면 그 매개변수는 두 끝을 포함한 그 사이의 값만 받는다.

examples/ch13/ranges.low

module ranges .
rem run: scale 100
rem trap: scale 101

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

실행 결과

$ lowentc --run scale ranges.low 100
scale(100) = 200
$ lowentc --run scale ranges.low 101
== ir diagnostics (1) ==
0:0 E-VM-CONTRACT: a parameter's declared range was violated at the program boundary (the value entered from outside — no caller proved it)

scale 의 본문은 a 가 100 이하라는 사실을 쓴다. 그래서 mul a. 2 . 는 200 을 넘지 않고, narrow u8 은 들어가지 않을 수 없으며, 두 자리의 넘침 검사가 모두 지워진다. 범위를 지키는 책임은 부르는 쪽에 있다. 넘기는 값의 타입이 이미 범위보다 넓으면(u64 따위) 번역이 거절되고, u8 처럼 들어갈 수 있는 타입의 값은 부르는 자리에서 범위를 확인한다. 프로그램 바깥에서 들어온 값(여기서는 --run 의 인자)도 경계에서 확인되고, 101 은 거기서 멈춘다. 명세는 부르는 쪽이 범위를 증명하지 못하면 번역을 거절한다고 적고 있으나, 이 판의 컴파일러는 증명되지 않은 u8 값을 실행 중 확인으로 넘긴다.

범위를 누가 어디서 재는지 한 장으로 보면 이렇다(101 리터럴은 이 판에서 실측했다).

 부르는 쪽이 넘기는 값                  scale 에 들어가기 전
 42 (리터럴)      ── 번역이 잰다 ───▶   통과
 101 (리터럴)     ── 번역이 잰다 ───▶   E-TYPE-RANGE (거절)
 u8 값            ── 부르는 자리 ───▶   101 이면 멈춤
 --run 의 인자    ── 경계에서 ──────▶   101 이면 멈춤
 u64 값           ── 이미 넓다 ─────▶   E-TYPE-WIDTH (거절)

같은 일을 requires le a. 100 . . 으로도 적을 수 있다. 차이는 어디에 적히는가다. range 는 매개변수의 생김새가 되어 부르는 쪽이 서명만 보고도 알고, 별칭(type pct range 0 100 .)에 실어 여러 op 에 물려줄 수 있다.

13.3 cast — 값이 변할 수 있는 자리#

widen 은 값이 변하지 않는 자리에만 쓴다. 부호를 바꾸거나 갈래를 넘는(부동소수점 ↔ 정수) 변환은 cast <타입> <값> 으로 적는다.

examples/ch13/casts.low

module casts .
rem run: truncate 7.9
rem run: truncate -2.9
rem run: to_signed 200
rem trap: to_signed 3000000000

fn truncate input x f64 . output i32 .
do
  return cast i32 x. . .
end .

fn to_signed input x u32 . output i32 .
do
  return cast i32 x. . .
end .

실행 결과

$ lowentc --run truncate casts.low 7.9
truncate(7.9) = 7
$ lowentc --run truncate casts.low -2.9
truncate(-2.9) = -2
$ lowentc --run to_signed casts.low 200
to_signed(200) = 200
$ lowentc --run to_signed casts.low 3000000000
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)

cast 는 “아무 일이나 해도 된다” 는 표시가 아니다. 여기서 값이 변할 수 있음을 내가 안다는 표시다. 값을 잃는 변환이 암묵적으로 일어나지 않는다는 원칙(4장)을 어기지 않는 것은, 저자가 그 낱말을 적었기 때문이다.

흔한 오해. cast 는 C 의 캐스트처럼 비트를 그대로 옮긴다

C 의 (int32_t)x 는 들어가지 않는 값을 구현에 따라 자르거나 감는다. Lowent 의 cast 는 값을 옮기고, 들어가지 않으면 멈춘다. 비트를 그대로 두고 읽는 법만 바꾸는 일은 따로 bit_cast 가 맡는다 (20장). 두 일을 한 낱말에 싣지 않는다.

13.4 bits — 1 부터 64 비트까지#

type <이름> bits <수> . 은 그 수만큼의 비트를 가진 정수 타입을 만든다. 폭은 2 의 거듭제곱일 필요가 없다.

examples/ch13/tiny.low

module tiny .
rem run: grow 1000
rem trap: grow 1023

type ten_bits bits 10 .

fn grow input x ten_bits . output ten_bits .
do
  return add x. 1 . .
end .

실행 결과

$ lowentc --run grow tiny.low 1000
grow(1000) = 1001
$ lowentc --run grow tiny.low 1023
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)

ten_bits 는 0 … 1023 이다. 1023 에 1 을 더하면 열 비트에서 넘치므로 멈춘다. 프로토콜의 비트 필드나 압축된 표를 다룰 때 폭이 곧 계약이 된다. 다만 기계의 낱말과 폭이 어긋나면 빠르다는 보장은 없다. 이 언어가 보장하는 것은 열 비트로 셈한 정확한 답이다.

13.5 바이트 배치를 못 박는다#

구조체의 배치는 보통 처리기가 기계에 맞게 정한다. 선으로 나가는 머리나 장치가 읽는 레지스터처럼 바이트가 바깥에서도 같은 뜻이어야 하면 배치를 적는다.

examples/ch13/packed.low

module packed .

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

fn header_kind input b slice u8 . output u64 .
do
  let v view wire_header view wire_header b. . .
  return widen u64 field v. kind . . .
end .

실행 결과

$ lowentc --check packed.low
== check: ok ==

magic 1 · length 2 · kind 9 를 담은 머리는 바이트로 이렇게 놓인다. big 이라 큰 자리가 앞이다.

 byte:    0    1    2    3    4    5    6
        ┌────┬────┬────┬────┬────┬────┬────┐
        │ 00 │ 00 │ 00 │ 01 │ 00 │ 02 │ 09 │
        └────┴────┴────┴────┴────┴────┴────┘
         └───── magic ─────┘ └─ length ┘ kind
          u32 big            u16 big     u8

칸 뒤에는 rw·ro·wo 같은 닿는 법도 적을 수 있다. 장치 레지스터를 비추는 구조체에서 쓰기 전용 칸을 읽으면 번역이 거절된다(30장). 배치를 못 박으면 그만큼 처리기가 고를 여지가 줄어드므로, 필요한 자리에만 박는다.

13.6 배치의 두 방향과 실패하는 뷰#

view 는 바이트를 그 배치의 값으로 읽는다. 반대 방향 — 값을 그 배치의 바이트로 만드는 일 — 은 encode 가 한다.

examples/ch13/encoded.low

module encoded .
rem run: header_bytes 9

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

rem `view` 의 반대 방향 --- 값을 그 배치의 바이트로 만든다
fn header_bytes input k u8 . output u64 .
do
  let h lit wire_header do magic 1 . length 2 . kind k. . end . .
  let bytes slice u8 encode wire_header h. . .
  return add mul cast u64 len bytes. . . 1000 . widen u64 idx bytes. 6 . . . .
end .

실행 결과

$ lowentc --run header_bytes encoded.low 9
header_bytes(9) = 7009

encode wire_header h. . 는 칸마다 적힌 바이트 차례(big)대로 일곱 바이트를 만든다. 결과 7009 는 길이 7 과 마지막 바이트 kind 의 9 를 한데 적은 것이다. 선으로 나갈 머리를 짓는 쪽이 encode, 받은 머리를 읽는 쪽이 view 다.

 바이트 00 00 00 01 00 02 09 ── view · try_view ──▶ wire_header 값
 (선 · 파일 · 장치)          ◀──── encode ───────── magic 1 · length 2 · kind 9

view 는 길이나 정렬이 맞지 않으면 멈춘다. 계약이 깨졌다고 보기 때문이다. 그런데 네트워크에서 온 바이트는 짧은 것이 흔한 일이다. 그런 자리에서는 같은 일을 하되 실패를 값으로 주는 try_view 를 쓴다(17장).

examples/ch13/tryview.low

module tryview .
rem run: kind_or_zero [1,2,3,4,0,5,9]
rem run: kind_or_zero [1,2,3]

struct wire_header do
  layout packed .
  magic u32 big .
  length u16 big .
  kind u8 .
end .

rem 바깥에서 온 바이트는 짧을 수 있다 --- 멈추는 `view` 대신 `none` 을 주는 `try_view`
fn kind_or_zero input b slice u8 . output u64 .
do
  let o option view wire_header try_view wire_header b. . .
  guard is_some o. . else return 0 . .
  let v view wire_header some_value o. . .
  return widen u64 field v. kind . . .
end .

실행 결과

$ lowentc --run kind_or_zero tryview.low [1,2,3,4,0,5,9]
kind_or_zero([1,2,3,4,0,5,9]) = 9
  arg0 (written) = [1,2,3,4,0,5,9]
$ lowentc --run kind_or_zero tryview.low [1,2,3]
kind_or_zero([1,2,3]) = 0
  arg0 (written) = [1,2,3]

일곱 바이트면 kind 9 를 읽고, 세 바이트면 none 을 받아 0 을 돌려준다. 멈추는 view 는 이미 확인한 안쪽에서, 값으로 알리는 try_view 는 경계에서 쓴다.

13.7 구조체 뷰는 무엇을 보는가#

뷰는 바이트를 베끼지 않는다. 바이트 위에 구조체의 모양을 얹은 창이다. 창으로 칸을 읽으면 그 자리의 바이트를 읽고, 칸에 쓰면 그 자리의 바이트에 쓴다. C 에서 바이트 배열의 주소를 구조체 포인터로 바꿔 쓰는 것과 같은 일인데, 길이와 정렬은 얹을 때 확인되고 무엇을 쓸 수 있는지는 번역이 따진다.

창은 타입에 그렇게 적는다. view pt 는 읽기 창, mut view pt 는 쓰기 창이다. 구조체 값의 타입 pt 와는 다른 타입이라서, 이름 하나만 보고도 그것이 바이트를 보는 창인지, 바이트와 따로 사는 값인지 안다. 아래 프로그램이 세 가지 모양을 보여 준다.

examples/ch13/structview.low

module structview .
rem run: write_through [1,2]
rem run: copy_by_name [1,2]
rem run: pass_mut [1,2]

struct pt do
  x u8 .
  y u8 .
end .

rem 쓰기 창(mut view)의 칸에 쓰면 그 자리의 바이트에 쓴다 --- b 가 [1,9] 가 된다
proc write_through input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return idx b. 1 . .
end .

rem `cast` 로 사본을 짓는다 --- k 에 써도 b 는 그대로
proc copy_by_name input b mut slice u8 . output u8 . effects none . do
  let h view pt view pt b. . .
  var k pt cast pt h. . .
  set field k. y . 7 .
  return idx b. 1 . .
end .

rem mut 입력에 건네면 받는 쪽의 쓰기가 b 에 닿는다
proc put input p mut pt . output u8 . effects none . do
  set field p. y . 6 .
  return 0 .
end .
proc pass_mut input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  let r u8 put h. . .
  return add idx b. 1 . r. . .
end .

실행 결과

$ lowentc --run write_through structview.low [1,2]
write_through([1,9]) = 9
  arg0 (written) = [1,9]
$ lowentc --run copy_by_name structview.low [1,2]
copy_by_name([1,2]) = 2
  arg0 (written) = [1,2]
$ lowentc --run pass_mut structview.low [1,2]
pass_mut([1,6]) = 6
  arg0 (written) = [1,6]
 b:  ┌────┬────┐            let h mut view pt view pt b. .
     │ 01 │ 02 │  ◀──────── h.x  h.y      (같은 바이트를 보는 창)
     └────┴────┘
       set field h. y . 9 .   →   b = [01, 09]

     let h view pt view pt b. .  ·  var k pt cast pt h. . .
       k = { x 1 · y 2 }      (사본 — b 와 따로 산다)
       set field k. y . 7 .   →   b = [01, 02] 그대로

창을 다른 창 이름에 묶으면(let k mut view pt h. .) 같은 바이트를 본다. 같은 바이트 위에 창을 둘 얹어도 둘은 서로의 쓰기를 본다. try_view 는 option view pt(또는 option mut view pt)를 주고, 그 some_value 도 창이다. 창 이름에 구조체 값을 통째로 넣는 것도 타입이 맞지 않아 거절된다 — 바이트를 바꾸려면 칸마다 set field 를 하거나 encode 로 지은 바이트를 copy 한다. 입력의 바이트를 보는 창을 돌려주는 op 은 output view pt 로 적는다.

13.7.1 고칠 수 있음은 창의 타입에 적힌다#

쓰기 창은 쓸 수 있는 바이트에서만 나온다. 읽기 전용으로 받은 슬라이스, mut 없이 묶은 슬라이스, 문자열 리터럴, 읽기 창, 읽기 창을 내는 op 의 결과에서 mut view 를 묶으면 번역이 E-TYPE-MUT 로 거절한다. 읽기 창으로 칸에 쓰면 E-TYPE-MUT, 읽기 창을 mut 입력에 건네면 E-TYPE-ARGMUT, 읽기 전용 입력의 쓰기 창을 돌려주면 E-TYPE-RETMUT 다. 읽기만 할 거라면 view pt 로 묶으면 된다.

fn 은 mut 으로 받은 바이트의 창에도 쓰지 못한다 — 직접 얹은 창이든 다른 op 이 돌려준 창이든 그 쓰기는 부른 쪽에 보이므로 fn 의 순수함이 깨진다(E-EFFECT-PURITY). 창으로 바이트를 고치는 op 은 proc 으로 적는다.

13.7.2 어떤 구조체에 뷰를 얹을 수 있나#

뷰는 바이트를 그대로 읽으므로 구조체가 정해진 바이트 배치를 가져야 하고, 그 칸에 어떤 바이트가 와도 뜻이 있는 값이어야 한다.

칸의 타입뷰까닭
u8…u64 · i8…i64 · f32 · f64된다폭이 정해져 있고 어느 비트열도 값이다
그런 수의 고정 배열 · 그런 칸만 가진 구조체된다배치가 구조체 안에 그대로 놓인다
booltry_view 만 — view 는 E-TYPE-BITCAST0 · 1 아닌 바이트가 오면 참도 거짓도 아닌 값이 된다 — try_view 가 잰다
폭을 적은 열거(layout u8 .)try_view 만 — view 는 E-TYPE-BITCAST어느 갈래의 번호도 아닌 수가 올 수 있다 — try_view 가 잰다
폭을 적지 않은 열거 · 슬라이스 · option · 소유 핸들안 된다 — E-IR-UNDEF정해진 바이트가 없다(그 구조체는 encode 도 못 한다)

표 13.1 — 구조체 뷰를 얹을 수 있는 칸

bool 칸이 왜 문제인지는 직접 재 볼 수 있다. 막기 전에는 바이트 2 를 읽은 칸이 if 로는 참, eq … true 로는 거짓, not 으로도 거짓이었다. bit_cast bool 을 거절하는 것과 같은 까닭이다(20장). view_array 로 그런 구조체나 bool 을 얹는 것도 같은 까닭으로 거절된다. 값을 바이트로 적어 내리는 encode 에는 이 제한이 없다 — 제 값을 쓰기 때문이다.

13.7.3 try_view 는 칸을 잰다#

바깥에서 온 바이트를 bool 이나 열거로 읽고 싶으면 try_view 를 쓴다. try_view 는 길이와 정렬에 더해 그런 칸을 잰다. bool 칸의 바이트가 0 · 1 이 아니거나, 열거 칸의 번호가 어느 갈래의 번호도 아니면 none 을 준다. 안에 든 구조체의 칸도 잰다.

열거를 바이트에 두려면 열거 선언에 폭을 적는다 — layout u8 . 한 줄이다(10장). 바이트에는 갈래의 번호가 들고, 읽으면 그 번호의 갈래가, 쓰면 그 갈래의 번호가 된다. 폭을 처리기가 고르지 않는 까닭은 갈래 하나를 더하는 일이 선 너머의 배치를 말없이 바꾸면 안 되기 때문이다.

examples/ch13/trycheck.low

module trycheck .
rem run: kind_of [7,1,2]
rem run: kind_of [2,1,2]
rem run: active_of [1,5]
rem run: active_of [2,5]

rem 폭을 적은 열거 --- 바이트 하나에 갈래의 번호가 든다
enum kind do
  layout u8 .
  ping .
  data 7 .
  close .
end .

struct hdr do
  layout packed .
  k kind .
  len u16 big .
end .

struct flag do
  active bool .
  n u8 .
end .

rem 번호가 1 · 7 · 8 이 아니면 try_view 가 none 을 준다
fn kind_of input b slice u8 . output u8 . do
  let o option view hdr try_view hdr b. . .
  guard is_some o. . else return 0 . .
  let h view hdr some_value o. . .
  match field h. k . do
    case ping do return 1 . end .
    case data do return 2 . end .
    case close do return 3 . end .
  end .
end .

rem bool 칸의 바이트가 0 · 1 이 아니어도 none 이다
fn active_of input b slice u8 . output u8 . do
  let o option view flag try_view flag b. . .
  guard is_some o. . else return 9 . .
  let h view flag some_value o. . .
  if field h. active . do return 1 . end .
  return 0 .
end .

실행 결과

$ lowentc --run kind_of trycheck.low [7,1,2]
kind_of([7,1,2]) = 2
  arg0 (written) = [7,1,2]
$ lowentc --run kind_of trycheck.low [2,1,2]
kind_of([2,1,2]) = 0
  arg0 (written) = [2,1,2]
$ lowentc --run active_of trycheck.low [1,5]
active_of([1,5]) = 1
  arg0 (written) = [1,5]
$ lowentc --run active_of trycheck.low [2,5]
active_of([2,5]) = 9
  arg0 (written) = [2,5]

kind_of 는 바이트 0 의 7 을 data 로 읽어 2 를 돌려주고, 어느 갈래의 번호도 아닌 2 에서는 none 을 받아 0 을 돌려준다. active_of 는 바이트 2 를 bool 로 받지 않고 none 을 받는다. 잰 뒤에 아래 바이트가 다른 길로 바뀌어 어느 갈래의 번호도 아니게 되면, 그 칸을 읽는 순간 멈춘다.

길이와 정렬의 조건은 앞 절 그대로다. view 는 모자라면 멈추고(E-VM-VIEW · E-VM-ALIGN), try_view 는 none 을 준다.

13.8 bitset — 작은 수의 집합#

bitset <비트 수> 는 0 부터 그 수 미만까지의 수가 들어 있는지 없는지를 담는 집합이다. bits(한 정수의 폭)와 이름이 비슷하지만 뜻이 다르다. bitset 은 워드의 비트를 다루는 도구가 아니라 집합이고, 워드의 비트는 bit_and·shl 같은 비트 연산으로 다룬다.

examples/ch13/sets.low

module sets .
rem run: overlap 5
rem run: overlap 3
rem run: flipped

rem 0 … 63 가운데 들어 있는 수의 모음 --- 워드의 비트가 아니라 집합이다
fn overlap input n u64 . output usize .
  requires le n. 60 . .
do
  var a bitset 64 bitset_new 64 . .
  var b bitset 64 bitset_new 64 . .
  bitset_insert a. 1 .
  bitset_insert a. 3 .
  bitset_insert a. 5 .
  bitset_insert b. 3 .
  bitset_insert b. n. .
  let both bitset 64 bitset_intersect a. b. . .
  let only_a bitset 64 bitset_difference a. b. . .
  rem 집합에서 1 을 지운다 --- 제자리에서 바뀐다
  bitset_remove a. 1 .
  var code usize add mul bitset_count both. . 100 . mul bitset_count only_a. . 10 . . .
  if bitset_is_subset both. a. . do set code. add code. 1 . . end .
  if bitset_is_empty only_a. . do set code. add code. 1000 . . end .
  return code. .
end .

rem 여집합은 폭 안에서만 뜻이 있다 --- 여덟 칸 가운데 하나를 넣었으면 나머지 일곱
fn flipped output usize .
do
  var a bitset 8 bitset_new 8 . .
  bitset_insert a. 0 .
  return bitset_count bitset_complement a. . . .
end .

실행 결과

$ lowentc --run overlap sets.low 5
overlap(5) = 211
$ lowentc --run overlap sets.low 3
overlap(3) = 121
$ lowentc --run flipped sets.low
flipped() = 7

overlap 5 에서 교집합은 {3, 5}, a 에만 있는 것은 {1} 이라 211 이 나온다. bitset_complement 는 폭 안에서만 뜻이 있다. 여덟 칸 집합에 0 하나를 넣었으면 여집합은 나머지 일곱이다. 폭 밖의 수를 넣거나 물으면 멈춘다 — 집합의 범위도 타입의 일부이기 때문이다.

13.9 흩어진 조각을 한 눈으로 — segments#

바이트가 한 줄로 이어져 있지 않고 여러 조각에 흩어져 있을 때가 있다. 네트워크에서 받은 조각들이나 링 버퍼의 앞뒤가 그렇다. 그것을 한 자리로 모으려면 베껴야 한다. segments 는 베끼지 않고 조각들을 하나의 눈으로 본다.

examples/ch13/pieces.low

module pieces .
rem run: main

use segview .

proc main input al cap allocator . output u8 . effects alloc .
do
  let mb option mut slice u8 alloc_bytes al. capacity 16 . .
  guard is_some mb. . else return 250 . .
  let md option mut slice u8 alloc_bytes al. capacity 32 . .
  guard is_some md. . else return 251 . .
  rem 뒤에 놓인 바이트 16 개
  var back mut slice u8 some_value mb. . .
  set idx back. 4 . 20 .
  set idx back. 9 . 30 .
  rem 서술자 — (어디부터, 얼마) 짝이 둘: 0 부터 4 개, 8 부터 4 개
  var d mut slice u64 view_array u64 some_value md. . . .
  set idx d. 0 . 0 .
  set idx d. 1 . 4 .
  set idx d. 2 . 8 .
  set idx d. 3 . 4 .
  rem 복사 없이 두 조각을 한 눈으로 본다
  let ss segments u8 view_segments back. d. . .
  let p slice u8 segview.piece_at ss. 1 . .
  rem 조각 수 2 에 10 을 곱하고, 둘째 조각의 1 번 바이트(원래 9 번 자리의 30)를 더한다
  return cast u8 add mul segview.piece_count ss. . 10 . cast usize idx p. 1 . . . . .
end .

실행 결과

$ lowentc --run main pieces.low
main() = 50

13.10 타입 낱말의 세 갈래#

타입 자리에 올 수 있는 낱말은 정본이 닫힌 목록으로 정한다. 그 가운데 일부는 이름만 알고 뜻이 아직 없다.

examples/ch13/notyet.low

module notyet .
rem expect: W-NOT-YET

rem byte 는 이름만 아는 타입 낱말이다 — 뜻이 없어 경고한다. 바이트는 u8 로 적는다
fn first input x byte . output u64 . do
  return 0 .
end .

rem 문법 부록 A.9 의 `list` 도 같다 — 목록에는 있었으나 뜻을 정한 조항이 없다
fn count input xs list u64 . output u64 .
do
  return 0 .
end .

실행 결과

$ lowentc --check notyet.low
notyet.low:5:0 W-NOT-YET: this type NAME is accepted but carries NO MEANING yet: lowering cannot read it, so every op in its signature falls to the interpreter (~80x). The answer stays right, so no oracle will ever see this. Declare it (`type id u64 .`) or write the underlying type
notyet.low:10:0 W-NOT-YET: this type NAME is accepted but carries NO MEANING yet: lowering cannot read it, so every op in its signature falls to the interpreter (~80x). The answer stays right, so no oracle will ever see this. Declare it (`type id u64 .`) or write the underlying type

byte 는 받아들이되 W-NOT-YET 으로 “뜻이 없다” 고 말한다. 조용히 받아 주면 쓰는 사람은 된 줄 안다. 바이트는 u8 로 적는다.

갈래낱말도구가 하는 일
쓸 수 있다수 · bool · void · slice · array · segments · set · stack · range · vec · bitset · mask · result · option · ref · mut_ref · mut · owned · region · cap · self뜻대로 검사하고 낮춘다
이름만 안다byte · char · str · string · bytes_view · dyn · atomicW-NOT-YET — 뜻이 없다고 말한다
아직 거절shared_read · lock · rwlockE-LOCK-NOTYET — 흐름끼리 나누는 상태(26장)
한정자unsafe_ptr타입 앞에 붙는 C 포인터 표시(29장)
정본에 뜻이 없다list · raw · addr · rng이 판의 도구는 경고 없이 받는다 — 쓰지 않는다

표 13.2 — 타입 낱말 — 쓸 수 있는 것 · 이름만 아는 것 · 거절되는 것

마지막 줄은 이 판의 구멍이다. 네 낱말은 정본의 목록에 있지만 뜻을 정한 조항이 없고, 도구는 아무 말 없이 받아들인다. 개발 저장소에 결함으로 적어 두었다.

13.11 흔한 실수#

반례. 넓은 타입의 값을 range 매개변수에 그대로 넘긴다

examples/ch13/mistake_rangewide.low

module mistake_rangewide .
rem expect: E-TYPE-WIDTH

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

fn call_scale input x u64 . output u8 .
do
  rem ✘ `u64` 는 0 … 100 보다 넓다 --- 범위 안이라는 것을 부르는 쪽이 보이지 않았다
  return scale x. . .
end .

실행 결과

$ lowentc --check mistake_rangewide.low
mistake_rangewide.low:12:0 E-TYPE-WIDTH: value does not fit the declared type (a literal out of range, or a narrowing — use narrow / narrow_wrap / narrow_sat / round_to)

scale 은 0 … 100 만 받는다고 서명에 적었다. u64 값은 그보다 넓으므로, 범위 안이라는 것을 부르는 쪽이 보이지 않으면 번역이 E-TYPE-WIDTH 로 거절한다. 거절하는 까닭은 책임의 자리를 정하기 위해서다. 범위를 벗어난 값이 오면 무엇을 할지는 부르는 쪽만 안다. guard 로 범위를 확인해 밖이면 따로 답하고, 안이면 narrow 로 좁혀 넘긴다.

examples/ch13/rangewide_fixed.low

module rangewide_fixed .
rem run: call_scale 40
rem run: call_scale 400

fn scale input a range 0 100 . output u8 .
do
  return narrow u8 mul a. 2 . . .
end .

fn call_scale input x u64 . output u8 .
do
  rem 범위 밖이면 여기서 따로 답하고, 안이면 좁혀서 넘긴다
  guard le x. 100 . else return 255 . .
  return scale narrow u8 x. . . .
end .

실행 결과

$ lowentc --run call_scale rangewide_fixed.low 40
call_scale(40) = 80
$ lowentc --run call_scale rangewide_fixed.low 400
call_scale(400) = 255

반례. 부호 있는 수를 widen 으로 부호 없는 타입에 넣는다

examples/ch13/mistake_widensign.low

module mistake_widensign .
rem expect: E-WIDEN-SIGN

fn to_count input x i32 . output u64 .
do
  rem ✘ 음수는 `u64` 에 자리가 없다 --- 폭이 넓어도 `widen` 은 안 된다
  return widen u64 x. . .
end .

실행 결과

$ lowentc --check mistake_widensign.low
mistake_widensign.low:7:0 E-WIDEN-SIGN: a signed integer cannot widen into an unsigned one — a negative value has no place there. Use `cast` if that is what you mean

i32 에서 u64 로 가면 폭은 넓어지지만, −1 같은 음수는 u64 에 자리가 없다. widen 은 어떤 값도 변하지 않는 자리에만 쓰므로 E-WIDEN-SIGN 이다. 음수가 올 수 없다고 알고 있다면 cast u64 x. . 라고 적어 그 판단을 소스에 남긴다. 음수가 오면 cast 가 멈춘다. 음수를 따로 다뤄야 한다면 guard ge x 0 을 먼저 둔다.

반례. 슬라이스 · 배열 · 매트릭스에 type 이나 newtype 으로 이름을 붙인다

examples/ch13/mistake_rowname.low

module mistake_rowname .
rem expect: E-TYPE-ROWNAME

rem ✘ 슬라이스에는 다른 이름을 붙이지 못한다 --- `slice u8` 를 그대로 적는다
type bytes slice u8 .

fn first input b bytes . output u8 .
  requires gt len b. . 0 . .
do
  return idx b. 0 . .
end .

실행 결과

$ lowentc --check mistake_rowname.low
5:12 E-TYPE-ROWNAME: a row type — `slice`, `array`, `matrix` — is not given another name: `type` and `newtype` do not take one as the whole type. Write `slice <type>` where it is used; to give a row a name of its own, make it a struct field (`struct name do bytes slice u8 . end .`). A row INSIDE another type may be named (`type maybe option slice u8 .`)

slice · array · matrix 는 원소 타입과 길이 · 차원이 낱말에 그대로 드러나는 타입이다. 여기에 다른 이름을 붙이면 읽는 사람이 그 이름 뒤에 무엇이 있는지 선언을 찾아가야 안다. 그래서 type 과 newtype 은 이 셋을 타입 전체로 받지 않는다 — 슬라이스는 E-TYPE-ROWNAME, 배열은 E-TYPE-ARRAY, 매트릭스는 E-MATRIX-USE 로 거절된다. 쓰는 자리에 타입을 그대로 적는다. 다른 타입 안에 든 것은 이름을 붙일 수 있다(type found option slice u8 .). 제 이름을 가진 타입이 필요하면 구조체의 칸으로 둔다 — 구조체는 이름이 다르면 다른 타입이다.

examples/ch13/rowname_fixed.low

module rowname_fixed .

rem 이름이 필요하면 구조체의 칸으로 둔다 --- 이름이 다르면 다른 타입이다
struct packet do body slice u8 . end .

fn first input b slice u8 . output u8 .
  requires gt len b. . 0 . .
do
  return idx b. 0 . .
end .

fn size input p packet . output usize .
do
  return len field p. body . . .
end .

실행 결과

$ lowentc --check rowname_fixed.low
== check: ok ==

반례. 수를 cast 로 bool 로 바꾼다

examples/ch13/mistake_boolcast.low

module mistake_boolcast .
rem expect: E-TYPE-KIND

fn nonzero input n u64 . output bool .
do
  rem ✘ 수의 어느 값이 참인지는 물음이 정한다 --- `ne n. 0 .` 으로 적는다
  return cast bool n. . .
end .

실행 결과

$ lowentc --check mistake_boolcast.low
mistake_boolcast.low:7:0 E-TYPE-KIND: bool is not numeric: it cannot be a cast target — write the question: `ne n. 0 .` (or `eq n. 0 .`)

C 는 0 이 아닌 수를 참으로 보지만, 수의 어느 값이 참인지는 그 자리에서 묻는 것이 정한다. 그래서 bool 은 cast 의 대상이 될 수 없고 E-TYPE-KIND 로 거절된다. ne n. 0 . 처럼 물음을 적으면 그 한 줄이 뜻을 소스에 남긴다. 반대 방향은 된다 — cast u8 b. . 는 참이면 1, 거짓이면 0 이다.

반례. 읽기 전용으로 받은 바이트에서 쓰기 창을 묶는다

examples/ch13/mistake_viewro.low

module mistake_viewro .
rem expect: E-TYPE-MUT

struct pt do
  x u8 .
  y u8 .
end .

fn mark input b slice u8 . output u8 . do
  rem ✘ 읽기 전용으로 받은 바이트에서 쓰기 창(mut view)을 묶는다 --- 칸에 쓰면 부른 쪽의 b 가 바뀐다
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return 0 .
end .

실행 결과

$ lowentc --check mistake_viewro.low
mistake_viewro.low:11:0 E-TYPE-MUT: a write window (`mut view`) comes only from bytes that may be written — these are read-only (a shared input, a `let` slice, a literal, a read window or an op that gives one). Bind `view <type>` to read, or take the bytes as `mut` (canon 6.2.13 (7))

창은 사본이 아니므로 h 의 칸에 쓰는 것은 b 에 쓰는 것이다. 그런데 b 는 읽기 전용으로 받았다 — 부른 쪽은 이 op 이 자기 바이트를 안 바꾼다고 믿는다. 그래서 mut view 를 묶는 자리에서 E-TYPE-MUT 로 거절된다. 읽기만 하면 view pt 로 묶고, 고쳐야 하면 바이트를 mut 으로 받는다. 그러면 부르는 쪽이 서명에서 그것을 본다.

examples/ch13/viewro_fixed.low

module viewro_fixed .
rem run: peek [1,2]
rem run: mark [1,2]

struct pt do
  x u8 .
  y u8 .
end .

rem 읽기만 하면 읽기 창(view)으로 묶는다
fn peek input b slice u8 . output u8 . do
  let h view pt view pt b. . .
  return field h. y . .
end .

rem 고치려면 바이트를 mut 으로 받는다 --- 그러면 부르는 쪽이 그것을 서명에서 본다
proc mark input b mut slice u8 . output u8 . effects none . do
  let h mut view pt view pt b. . .
  set field h. y . 9 .
  return idx b. 1 . .
end .

실행 결과

$ lowentc --run peek viewro_fixed.low [1,2]
peek([1,2]) = 2
  arg0 (written) = [1,2]
$ lowentc --run mark viewro_fixed.low [1,2]
mark([1,9]) = 9
  arg0 (written) = [1,9]

반례. 뷰로 읽을 구조체에 bool 칸을 둔다

examples/ch13/mistake_viewbool.low

module mistake_viewbool .
rem expect: E-TYPE-BITCAST

struct flag do
  active bool .
  n u8 .
end .

fn is_on input b slice u8 . output bool . do
  rem ✘ bool 칸에 0 · 1 아닌 바이트가 오면 참도 거짓도 아닌 값이 된다
  let h view flag view flag b. . .
  return field h. active . .
end .

실행 결과

$ lowentc --check mistake_viewbool.low
mistake_viewbool.low:11:0 E-TYPE-BITCAST: a view reads the bytes as they are, and this struct has a `bool` or enum field — a byte other than 0 or 1 would be a bool that is neither true nor false, a number no variant has would be no variant. Every field of a viewed struct must be `plain` (canon 8.7.1, as for `bit_cast`): use `try_view`, which checks such fields and gives `none` for bad bytes, or declare the field as a number

바깥에서 온 바이트에는 0 과 1 말고도 아무 값이나 올 수 있다. bool 칸에 2 가 오면 참도 거짓도 아닌 값이 되므로 E-TYPE-BITCAST 다. 틀린 입력을 걸러 내려면 try_view 로 얹는다(위 절 — 2 가 오면 none). 2 를 참으로 볼 생각이라면 칸을 u8 로 받고 물음을 적는다. 그러면 2 를 참으로 볼지 틀린 입력으로 볼지가 소스에 남는다.

examples/ch13/viewbool_fixed.low

module viewbool_fixed .
rem run: is_on [2,5]
rem run: is_on [0,5]

struct flag do
  active u8 .
  n u8 .
end .

rem 칸을 u8 로 받고 무엇을 참으로 볼지는 물음으로 적는다
fn is_on input b slice u8 . output bool . do
  let h view flag view flag b. . .
  return ne field h. active . 0 . .
end .

실행 결과

$ lowentc --run is_on viewbool_fixed.low [2,5]
is_on([2,5]) = 1
  arg0 (written) = [2,5]
$ lowentc --run is_on viewbool_fixed.low [0,5]
is_on([0,5]) = 0
  arg0 (written) = [0,5]

흔한 오해. type 으로 이름을 달리하면 단위가 섞이지 않는다

examples/ch13/type_units.low

module type_units .
rem run: total 100 100

type meters u64 .
type feet u64 .

rem 미터와 피트를 더했는데 아무도 막지 않는다 --- `type` 은 다른 이름일 뿐이다
fn total input m meters . input f feet . output meters .
  requires le m. 1000000 . .
  requires le f. 1000000 . .
do
  return add m. f. . .
end .

실행 결과

$ lowentc --run total type_units.low 100 100
total(100, 100) = 200

meters 와 feet 는 둘 다 u64 의 다른 이름일 뿐이라서 더해도 아무도 막지 않는다. 100 미터와 100 피트의 합이 200 미터로 나온다. 섞이면 안 되는 뜻은 newtype 으로 가른다. 그러면 ids.low 처럼 번역이 E-TYPE-NOMINAL 로 막고, 건너는 자리는 cast 로 드러난다. type 은 긴 이름을 줄이거나 범위를 물려줄 때 쓴다.

흔한 오해. cast 로 음수를 부호 없는 타입에 넣으면 C 처럼 큰 수가 된다

examples/ch13/negcast.low

module negcast .
rem run: to_unsigned 5
rem trap: to_unsigned -1

fn to_unsigned input x i32 . output u32 .
do
  rem C 라면 -1 이 4294967295 가 된다 --- Lowent 는 들어가지 않으면 멈춘다
  return cast u32 x. . .
end .

실행 결과

$ lowentc --run to_unsigned negcast.low 5
to_unsigned(5) = 5
$ lowentc --run to_unsigned negcast.low -1
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)

C 에서 (uint32_t)-1 은 4294967295 다. 이 감김은 표준이 정한 동작이지만 대개 결함을 숨긴다. Lowent 의 cast 는 값을 옮기고, 들어가지 않으면 E-VM-CAST 로 멈춘다. 정말로 감기를 원한다면 narrow_wrap 처럼 그 뜻을 이름에 지닌 낱말을 쓴다(4장).

13.12 이 장의 문법을 한눈에#

모양뜻왜 이렇게
type meters u64 .같은 타입의 다른 이름긴 타입을 줄이고 뜻을 남긴다 — 섞임은 막지 않는다
newtype user_id u64 .표현이 같은 새 타입번호끼리 섞이는 결함을 번역이 막는다 — 실행 비용 없음
cast user_id n. . · cast u64 u. .새 타입과 원래 타입 사이를 건넌다건너는 자리가 코드에서 한곳에 모인다
input a range 0 100 .두 끝을 포함한 범위만 받는다계약이 서명의 생김새가 된다
type pct range 0 100 .범위를 별칭에 싣는다여러 op 이 같은 범위를 물려받는다
cast i32 x. .값이 변할 수 있는 변환 — 들어가지 않으면 멈춤“여기서 값이 변할 수 있음을 안다” 는 표시
type ten_bits bits 10 .1 … 64 비트 정수폭이 곧 계약
layout packed . · magic u32 big .채움 없는 배치 · 바이트 차례바깥과 바이트의 뜻을 맞춘다
view wire_header b. .바이트를 베끼지 않고 그 배치로 읽는다길이·정렬이 어긋나면 멈춘다
try_view wire_header b. . · encode wire_header h. .실패를 none 으로 주는 뷰 · 값을 그 배치의 바이트로경계에서는 멈추지 않는 쪽
let h mut view pt view pt b. . · let h view pt view pt b. . · cast pt h. .쓰기 창 · 읽기 창 · 바이트와 따로 사는 사본쓸 수 없는 바이트에서 mut view 는 거절된다
var a bitset 64 bitset_new 64 . . · bitset_insert a 1 . · bitset_intersect a. b. .작은 수의 집합과 그 연산집합이지 워드의 비트가 아니다
view_segments back. d. . · segview.piece_count ss. . · segview.piece_at ss. i. .흩어진 조각을 베끼지 않고 한 눈으로 · 조각 수 · i 번 조각모으는 복사를 없앤다
byte · lock 같은 타입 낱말W-NOT-YET · E-LOCK-NOTYET뜻이 없으면 없다고 말한다

표 13.3 — 이름 붙인 타입의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나

복습 정리

type 은 별칭이고 newtype 은 표현이 같은 새 타입이며, 둘 사이는 cast 로 건넌다. range lo hi 는 매개변수의 생김새가 된 계약으로, 부르는 쪽이 지키고 본문은 사실로 쓴다. cast 는 값이 변할 수 있는 변환을 적는 낱말이고 들어가지 않으면 멈춘다. bits N 은 임의 폭 정수다. layout packed 와 바이트 차례 표지, view 는 바이트 배치를 바깥과 맞춘다.