13 이름 붙인 타입 — type·newtype·range·cast
먼저 알아야 할 것
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 도 보게 된다.이 장에서 답할 질문
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 bytes slice u8 .), 뜻을 소스에 남길 때 쓴다. pct 는 범위가 붙은 타입의 별칭이고, 이 이름을 받는 모든 자리가 그 범위를 물려받는다(아래에서 다룬다).
타입 선언에는 be 를 끼우지 않는다.
examples/ch13/type_be.low
module type_be .
rem expect: E-TYPE-DECL
type pct be u8 .
실행 결과
$ lowentc --check type_be.low
type_be.low:4:0 E-TYPE-DECL: a type alias is `type N T .` and a newtype is `newtype N T .` — without `be`. `be` binds a VALUE (`let x u8 be 1 .`); a type declaration names a TYPE. One meaning, one spelling
be 는 let·var 가 값을 묶는 낱말이다. 여기서 묶는 것은 타입이다. 한 뜻에 한 표기다.
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 u64 .
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/isize) 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)
- 부동소수를 정수로 바꾸면 0 쪽으로 버린다. 7.9 는 7, −2.9 는 −2 다. 버려지는 소수부 때문에 멈추지는 않는다.
- 정수부나 값이 목표 타입에 들어가지 않으면 멈춘다. 30억은
i32에 들어가지 않는다. bool은cast의 대상도 원천도 될 수 없다. 참거짓과 수를 오가려면if로 적는다.
cast 는 “아무 일이나 해도 된다” 는 표시가 아니다. 여기서 값이 변할 수 있음을 내가 안다는 표시다. 값을 잃는 변환이 암묵적으로 일어나지 않는다는 원칙(4장)을 어기지 않는 것은, 저자가 그 낱말을 적었기 때문이다.
흔한 오해. cast 는 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 .
type bytes slice u8 .
struct wire_header do
layout packed .
magic u32 big .
length u16 big .
kind u8 .
end
fn header_kind input b bytes . output u64 .
do
let v view wire_header be view wire_header b .
return widen u64 (field v kind) .
end
실행 결과
$ lowentc --check packed.low
== check: ok ==
layout packed .은 칸 사이에 채움(padding)을 두지 않는다. 칸이 선언 차례로 맞붙는다.- 칸 뒤의
big·little은 바이트 차례를 정한다. 적지 않으면 그 기계의 차례를 따른다. view wire_header b는 바이트 슬라이스를 베끼지 않고 그 배치의 값으로 읽는다. 길이와 정렬이 맞지 않으면 멈춘다.
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 wire_header be make wire_header do magic 1 . length 2 . kind k . end .
let bytes slice u8 be encode wire_header h .
return add (mul (len bytes) 1000) (widen u64 (index 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 9view 는 길이나 정렬이 맞지 않으면 멈춘다. 계약이 깨졌다고 보기 때문이다. 그런데 네트워크에서 온 바이트는 짧은 것이 흔한 일이다. 그런 자리에서는 같은 일을 하되 실패를 값으로 주는 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 be try_view wire_header b .
guard is_some o . else return 0 .
let v view wire_header be 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 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 u64 .
requires le n 60 .
do
var a bitset 64 be bitset_new 64 .
var b bitset 64 be bitset_new 64 .
add a 1 .
add a 3 .
add a 5 .
add b 3 .
add b n .
let both bitset 64 be intersect a b .
let only_a bitset 64 be difference a b .
rem 집합에서 1 을 지운다 --- 제자리에서 바뀐다
remove a 1 .
var code u64 be add (mul (count both) 100) (mul (count only_a) 10) .
if is_subset both a . do set code (add code 1) . end
if is_empty only_a . do set code (add code 1000) . end
return code .
end
rem 여집합은 폭 안에서만 뜻이 있다 --- 여덟 칸 가운데 하나를 넣었으면 나머지 일곱
fn flipped output u64 .
do
var a bitset 8 be bitset_new 8 .
add a 0 .
return count (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
bitset_new 64가 빈 집합을 만든다. 폭은 번역 시점에 정해진 수다.add a 1 .은 넣고remove a 1 .은 지운다. 둘 다 제자리에서 집합을 바꾸는 문장이다.count a는 원소 수다.intersect a b는 교집합,difference a b는a에만 있는 것,complement a는 여집합을 새 집합으로 준다.is_subset x y는x가y에 다 들어 있는지,is_empty x는 비었는지 묻는다.
overlap 5 에서 교집합은 {3, 5}, a 에만 있는 것은 {1} 이라 211 이 나온다. complement 는 폭 안에서만 뜻이 있다. 여덟 칸 집합에 0 하나를 넣었으면 여집합은 나머지 일곱이다. 폭 밖의 수를 넣거나 물으면 멈춘다 — 집합의 범위도 타입의 일부이기 때문이다.
13.8 흩어진 조각을 한 눈으로 — segments#
바이트가 한 줄로 이어져 있지 않고 여러 조각에 흩어져 있을 때가 있다. 네트워크에서 받은 조각들이나 링 버퍼의 앞뒤가 그렇다. 그것을 한 자리로 모으려면 베껴야 한다. segments 는 베끼지 않고 조각들을 하나의 눈으로 본다.
examples/ch13/pieces.low
module pieces .
rem run: main
proc main input al cap allocator . output u8 . effects alloc .
do
let mb option mut slice u8 . . be alloc_bytes al capacity 16 .
guard is_some mb . else return 250 .
let md option mut slice u8 . . be alloc_bytes al capacity 32 .
guard is_some md . else return 251 .
rem 뒤에 놓인 바이트 16 개
var back mut slice u8 . be some_value mb .
set (index back 4) 20 .
set (index back 9) 30 .
rem 서술자 — (어디부터, 얼마) 짝이 둘: 0 부터 4 개, 8 부터 4 개
var d mut slice u64 . be view_array u64 (some_value md) .
set (index d 0) 0 .
set (index d 1) 4 .
set (index d 2) 8 .
set (index d 3) 4 .
rem 복사 없이 두 조각을 한 눈으로 본다
let ss segments u8 be view_segments back d .
let p slice u8 be seg ss 1 .
rem 조각 수 2 에 10 을 곱하고, 둘째 조각의 1 번 바이트(원래 9 번 자리의 30)를 더한다
return narrow u8 (add (mul (segs ss) 10) (index p 1)) .
end
실행 결과
$ lowentc --run main pieces.low
main() = 50
view_segments back d는 뒤에 놓인 바이트back과 서술자d를 묶어segments u8타입의 눈을 만든다. 서술자는 (어디부터, 얼마) 짝의 줄이다. 여기서는 0 부터 4 바이트, 8 부터 4 바이트다.segs ss는 조각 수 2 를,seg ss 1은 둘째 조각을 평범한slice u8로 준다. 둘째 조각의 1 번 바이트는 원래 9 번 자리의 30 이다.- 새 기계 명령은 없다. 묶음 짓기와 칸 읽기와 잘라내기로 낮아지므로 비용이 보인다.
13.9 타입 낱말의 세 갈래#
타입 자리에 올 수 있는 낱말은 정본이 닫힌 목록으로 정한다. 그 가운데 일부는 이름만 알고 뜻이 아직 없다.
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:10:0 E-NAME-BUILTIN: this name is a BUILTIN — the resolver always picks the builtin, so your declaration can never be called: it exists and does not exist. The namespace is FLAT (no shadowing). Rename it
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 str slice u8 .`) 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 str slice u8 .`) 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 · atomic | W-NOT-YET — 뜻이 없다고 말한다 |
| 아직 거절 | shared_read · lock · rwlock | E-LOCK-NOTYET — 흐름끼리 나누는 상태(26장) |
| 한정자 | unsafe_ptr | 타입 앞에 붙는 C 포인터 표시(29장) |
| 정본에 뜻이 없다 | list · raw · addr · rng | 이 판의 도구는 경고 없이 받는다 — 쓰지 않는다 |
표 13.1 — 타입 낱말 — 쓸 수 있는 것 · 이름만 아는 것 · 거절되는 것
마지막 줄은 이 판의 구멍이다. 네 낱말은 정본의 목록에 있지만 뜻을 정한 조항이 없고, 도구는 아무 말 없이 받아들인다. 개발 저장소에 결함으로 적어 두었다.
13.10 흔한 실수#
반례. 넓은 타입의 값을 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 . 을 먼저 둔다.
반례. bool 을 cast 로 수로 바꾼다
examples/ch13/mistake_boolcast.low
module mistake_boolcast .
rem expect: E-TYPE-KIND
fn flag input b bool . output u8 .
do
rem ✘ 참이 1 인지는 언어가 아니라 프로그램이 정할 일이다
return cast u8 b .
end
실행 결과
$ lowentc --check mistake_boolcast.low
mistake_boolcast.low:7:0 E-TYPE-KIND: bool is not numeric: it cannot be a cast SOURCE either. Which side is `1` is for the PROGRAM to say, not the language — write `if b . do 1 . else 0 .`
C 는 참을 1 로 보지만, 참이 1 인지 0 인지 −1 인지는 프로그램의 약속이지 언어가 정할 일이 아니다. 그래서 bool 은 cast 의 원천도 대상도 될 수 없고 E-TYPE-KIND 로 거절된다. 도구가 알려 주는 대로 if 로 “참이면 1, 거짓이면 0” 을 적는다. 그 한 줄이 약속을 소스에 남긴다.
흔한 오해. 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.11 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
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 으로 주는 뷰 · 값을 그 배치의 바이트로 | 경계에서는 멈추지 않는 쪽 |
var a bitset 64 be bitset_new 64 . · add a 1 . · intersect a b | 작은 수의 집합과 그 연산 | 집합이지 워드의 비트가 아니다 |
view_segments back d · segs ss · seg ss i | 흩어진 조각을 베끼지 않고 한 눈으로 · 조각 수 · i 번 조각 | 모으는 복사를 없앤다 |
byte · lock 같은 타입 낱말 | W-NOT-YET · E-LOCK-NOTYET | 뜻이 없으면 없다고 말한다 |
표 13.2 — 이름 붙인 타입의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
type 은 별칭이고 newtype 은 표현이 같은 새 타입이며, 둘 사이는 cast 로 건넌다. range lo hi 는 매개변수의 생김새가 된 계약으로, 부르는 쪽이 지키고 본문은 사실로 쓴다. cast 는 값이 변할 수 있는 변환을 적는 낱말이고 들어가지 않으면 멈춘다. bits N 은 임의 폭 정수다. layout packed 와 바이트 차례 표지, view 는 바이트 배치를 바깥과 맞춘다.