4 수 — 폭이 정해진 정수와 부동소수점
먼저 알아야 할 것
돌아보기
3장에서 let x u8 300 . 은 어떻게 되었는가? C 는 같은 일을 어떻게 처리하는가?
답. E-TYPE-WIDTH 로 거절되었다. 300 은 u8 의 범위 0 … 255 밖이기 때문이다. C 는 조용히 잘라 44 로 만든다. 그러면 소스에 적힌 값과 실제 값이 달라진다. 이 장은 리터럴이 아니라 계산 결과가 폭을 넘을 때 같은 원칙이 어떻게 적용되는지를 본다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
narrow_wrap이 C 와 같은 일을 한다면, 기본을 그렇게 두지 않은 이유가 무엇인가?- 비트 연산은 왜
expr섬에서 중위로 쓸 수 없는가? cast를 매번 적는 것은 번거롭지 않은가?
4.1 정수 타입은 이름에 폭을 적는다#
정수 타입의 이름은 부호와 비트 폭이다. u 는 부호 없음, i 는 부호 있음이다.
| 타입 | 폭 | 범위 |
|---|---|---|
u8 · u16 · u32 · u64 | 8 · 16 · 32 · 64 | 0 … 2N−1 |
i8 · i16 · i32 · i64 | 8 · 16 · 32 · 64 | −2N−1 … 2N−1−1 |
usize · idiff | 16 이상 — 구현이 정한다 | 폭에 따른다(아래 「크기 타입」) |
u128 · i128 | 128 — 64 비트 대상에서만 | 위와 같은 꼴(아래 「128 비트 정수」) |
표 4.1 — 정수 타입과 범위
C 의 int 는 기계마다 크기가 다를 수 있지만, Lowent 의 u32 는 어디서나 32 비트다. 음수는 2 의 보수로 표현한다. usize 는 u64 와 폭이 같은 기계에서도 다른 타입이고 서로 자동으로 바뀌지 않는다 — 길이를 세는 수와 그냥 수를 섞는 자리를 소스에 드러내기 위해서다. 건너는 낱말은 cast 하나다(아래 「크기 타입」).
4.2 128 비트 정수#
u128 · i128 은 64 비트 대상(x86_64 · arm64 · riscv64 · win64)에서만 선다. 32 비트 대상에서 이 낱말을 쓰면 번역이 거부된다(E-INT128-TARGET). 64 비트 두 수의 곱 전체처럼 64 비트를 넘는 셈을 한 값으로 담을 때 쓴다.
fn top_half input a u64 . input b u64 . output u64 . do
let x u128 widen u128 a. . .
return cast u64 shr mul x. widen u128 b. . . 64 . . .
end .- 다른 폭에서 128 비트 자리로는 자동으로 넓히지 않는다 —
widen u128 …으로 적는다. 64 비트 칸의 값에는 위 절반이 없어서, 부호를 늘릴지 0 을 채울지를 넓히는 자리가 알아야 하기 때문이다. - 64 비트를 넘는 리터럴(
340282366920938463463374607431768211455)은 128 비트 자리에만 들어간다. - 지역 · 입력 · 결과 ·
option의 알맹이에 더해 구조체의 칸과 슬라이스 · 배열 · 뷰 · 스택의 원소에도 선다. 바이트 배치는 C 컴파일러의 128 비트 정수와 같다 — 크기 16 · 정렬 16 · 작은 끝이 먼저. 벡터(vec)의 원소와 C 경계에 두면E-INT128-PLACE다. - 명령줄로 주는 128 비트 수는 적은 그대로 읽고, 타입에 안 들어가는 수(
u128의 음수 · 2128 이상)는 거절한다. - 64 비트 곱의 위쪽 절반만 필요하면
bigint64.mul_hi로도 된다(128 비트 값을 짓지 않는다). 128 비트 정수는 지역 · 셈 · 입력 · 결과로 빠른 길에 선다. 구조체의 칸이나 배열 · 슬라이스 · 뷰 · 스택의 원소에 든 128 비트를 만지는 op 과, 넘침을 값으로 알리는chk_*·narrow_try를 128 비트에 쓰는 op 은 태그 경로로 내려간다(--why-slow가 그 까닭을 말한다).
4.3 넓히기는 자동, 좁히기는 적는다#
값을 잃지 않는 변환을 넓히기라 한다. u8 을 u32 자리에 넘기는 일은 값이 그대로이므로 컴파일러가 알아서 한다. 부호가 섞여도 값을 지키는 넓히기가 있으면 계산할 수 있다 — u8 은 i16 안에 모두 들어간다.
examples/ch04/widths.low
module widths .
rem run: mix 200 -100
rem run: shrink 200
rem trap: shrink 300
rem run: shrink_sat 300
rem run: shrink_wrap 300
fn mix input a u8 . input b i16 . output i16 .
do
return add a. b. . .
end .
fn shrink input x u64 . output u8 .
do
return narrow u8 x. . .
end .
fn shrink_sat input x u64 . output u8 .
do
return narrow_sat u8 x. . .
end .
fn shrink_wrap input x u64 . output u8 .
do
return narrow_wrap u8 x. . .
end .
실행 결과
$ lowentc --run mix widths.low 200 -100
mix(200, -100) = 100
$ lowentc --run shrink widths.low 200
shrink(200) = 200
$ lowentc --run shrink_sat widths.low 300
shrink_sat(300) = 255
$ lowentc --run shrink_wrap widths.low 300
shrink_wrap(300) = 44
$ lowentc --run shrink widths.low 300
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)
mix 는 u8 과 i16 을 더했고 결과는 넓은 쪽인 i16 이다. 넓히기를 눈에 보이게 적고 싶으면 widen u64 x. . 로 적는다. 같은 폭의 i32 와 u32 는 어느 쪽도 다른 쪽에 다 들어가지 않으므로 섞을 수 없다.
examples/ch04/sign_bad.low
module sign_bad .
rem expect: E-TYPE-SIGN
fn total input a i32 . input b u32 . output i32 .
do
return add a. b. . .
end .
실행 결과
$ lowentc --check sign_bad.low
sign_bad.low:6:0 E-TYPE-SIGN: sign mismatch: no value-preserving widening exists (widen both to a strictly wider signed type, or bitcast_sign)
값을 잃을 수 있는 변환은 좁히기이고 반드시 적는다. 같은 파일의 shrink 는 narrow u8 x. . 로 u64 를 u8 로 줄인다. 200 은 들어가서 그대로 나오지만, 300 은 들어가지 않으므로 실행이 멈춘다 (E-VM-CAST). 멈추지 않기를 원하면 원하는 처분을 이름으로 고른다. narrow_sat 는 끝값 255 에 서고, narrow_wrap 은 C 처럼 감아서 44 를 준다. narrow_try 는 들어가지 않으면 none 을 준다.
문. narrow_wrap 이 C 와 같은 일을 한다면, 기본을 그렇게 두지 않은 이유가 무엇인가?
답. 감기가 의도였는지 실수였는지를 소스에 남기려는 것이다. 기본이 감기면 narrow u8 x. . 가 44 를 준 자리를 읽는 사람이 그것이 원하던 값인지 알 수 없다. 기본이 멈춤이면, 감기를 원한 저자는 narrow_wrap 이라고 적었을 것이다. 적힌 이름이 곧 저자의 의도에 대한 기록이다.
4.4 넘치면 멈춘다#
산술은 선언된 폭에서 일어난다. 결과가 폭에 들어가지 않으면 멈춘다(트랩).
examples/ch04/overflow.low
module overflow .
rem run: bump 254
rem trap: bump 255
rem run: bump_wrap 255
rem run: bump_sat 255
fn bump input a u8 . output u8 .
do
return add a. 1 . .
end .
fn bump_wrap input a u8 . output u8 .
do
return wrap_add a. 1 . .
end .
fn bump_sat input a u8 . output u8 .
do
return sat_add a. 1 . .
end .
실행 결과
$ lowentc --run bump overflow.low 254
bump(254) = 255
$ lowentc --run bump_wrap overflow.low 255
bump_wrap(255) = 0
$ lowentc --run bump_sat overflow.low 255
bump_sat(255) = 255
$ lowentc --run bump overflow.low 255
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)
bump 254 는 255 로 들어가지만 bump 255 는 256 이 되어야 하므로 VM 이 E-VM-OVERFLOW 로 멈춘다. 네이티브도 같은 자리에서 멈추고 비영 종료 코드를 낸다(검증 스크립트는 둘 다 멈추는지를 확인한다). 처분을 바꾸려면 넓히기와 마찬가지로 이름을 고른다.
| 처분 | op | 넘치면 |
|---|---|---|
| 멈춤(기본) | add · sub · mul | 실행이 멈춘다 |
| 감김 | wrap_add · wrap_sub · wrap_mul | 폭 안에서 돌아 감는다 |
| 포화 | sat_add · sat_sub · sat_mul | 그 폭의 끝값에 선다 |
| 값으로 | chk_add · chk_sub · chk_mul | option 으로 알린다 |
표 4.2 — 넘침의 처분을 이름으로 고른다
“값으로” 알리는 쪽은 넘침을 프로그램이 다룰 수 있는 사건으로 만든다.
examples/ch04/chk.low
module chk .
rem run: safe_bump 255
rem run: safe_bump 7
fn safe_bump input a u8 . output u64 .
do
let r option u8 chk_add a. 1 . .
guard is_some r. . else return 999 . .
return widen u64 some_value r. . . .
end .
실행 결과
$ lowentc --run safe_bump chk.low 255
safe_bump(255) = 999
$ lowentc --run safe_bump chk.low 7
safe_bump(7) = 8
chk_add a. 1 . 은 option u8 을 준다. 넘치면 none 이고, guard is_some r else … 가 그 경우를 먼저 처리한다. 이 모양은 11장에서 다시 만난다.
흔한 오해. 넘침을 검사하면 느려진다
4.5 나눗셈과 나머지#
정수를 0 으로 나누면 멈춘다. 부호 있는 나눗셈은 0 쪽으로 버리고, 나머지 mod 의 부호는 나누는 수를 따른다.
examples/ch04/divide.low
module divide .
rem run: quot -7 2
rem run: modulo -7 2
rem trap: quot 1 0
rem run: safe_div 7 0
rem run: safe_div 7 2
fn quot input a i32 . input b i32 . output i32 .
do
return div a. b. . .
end .
fn modulo input a i32 . input b i32 . output i32 .
do
return mod a. b. . .
end .
fn safe_div input a u32 . input b u32 . output u32 .
do
let nz option nonzero u32 nonzero_of b. . .
guard is_some nz. . else return 0 . .
return div_nz a. some_value nz. . . .
end .
실행 결과
$ lowentc --run quot divide.low -7 2
quot(-7, 2) = -3
$ lowentc --run modulo divide.low -7 2
modulo(-7, 2) = 1
$ lowentc --run safe_div divide.low 7 0
safe_div(7, 0) = 0
$ lowentc --run safe_div divide.low 7 2
safe_div(7, 2) = 3
$ lowentc --run quot divide.low 1 0
== ir diagnostics (1) ==
0:0 E-VM-DIV0: divide by zero
quot -7 2 는 −3 이고 modulo -7 2 는 1 이다. 나머지가 나누는 수의 부호를 따르므로, 양수 n 으로 나눈 mod h. n. . 은 언제나 0 이상 n 미만이다. 해시 테이블이 슬롯을 고를 때 idx slots. mod h. n. . . 의 경계 검사가 지워지는 근거가 이 성질이고, 이 성질은 Coq 로 증명되어 있다(40장).
부호 있는 나눗셈이 넘치는 경우는 MIN / −1 하나뿐이고, 이것도 멈춘다. C 에서는 미정의 동작이다.
0 이 아님을 한 번 확인하고 그 사실을 들고 다니는 길도 있다. nonzero_of b. . 는 option nonzero u32 를 주고, 그 안의 값은 div_nz 에 넘길 수 있다. div_nz 자리에는 0 검사가 남지 않는다. 검사를 없앤 것이 아니라 한 자리로 옮기고 그 결과를 타입에 실은 것이다.
4.6 참거짓은 수가 아니다#
bool 은 true 와 false 두 값이다. 정수를 조건 자리에 쓰면 거절된다.
examples/ch04/cond_bad.low
module cond_bad .
rem expect: E-TYPE-COND
fn nonzero_flag input n u32 . output u8 .
do
if n. do return 1 . end .
return 0 .
end .
실행 결과
$ lowentc --check cond_bad.low
cond_bad.low:6:0 E-TYPE-COND: a condition must be bool, not a number — this language has no truthiness (D12: no implicit int↔bool). Say what the test is: `ne x 0`
C 의 if (n) 은 “n 이 0 이 아니다” 로 읽히지만, 읽는 사람은 그것이 “n 이 있다” 인지 “n 이 참이다” 인지 문맥으로 짐작해야 한다. Lowent 는 묻는 것을 적게 한다 — if gt n 0 이다. 반대로 bool 을 수처럼 더할 수도 없다. and·or·not 은 참거짓만 받고, and 와 or 는 앞쪽만으로 답이 정해지면 뒤쪽을 계산하지 않는다.
4.7 비트 연산#
비트 연산은 값을 비트의 나열로 보고 다룬다. 기호 대신 낱말이다.
examples/ch04/bits.low
module bits .
rem run: mask 12 10
rem run: flip 12
rem run: spin 129 1
rem run: s_shr -8 1
rem run: u_shr 4294967288 1
rem run: ones 7
rem 12 = 0000_1100 · 10 = 0000_1010
fn mask input a u8 . input b u8 . output u8 . do return bit_and a. b. . . end .
fn flip input a u8 . output u8 . do return bit_not a. . . end .
fn spin input a u8 . input n u8 . output u8 . do return rotl a. n. . . end .
fn s_shr input a i32 . input n u32 . output i32 . do return shr a. n. . . end .
fn u_shr input a u32 . input n u32 . output u32 . do return shr a. n. . . end .
fn ones input a u8 . output u8 . do return count_ones a. . . end .
실행 결과
$ lowentc --run mask bits.low 12 10
mask(12, 10) = 8
$ lowentc --run flip bits.low 12
flip(12) = 243
$ lowentc --run spin bits.low 129 1
spin(129, 1) = 3
$ lowentc --run s_shr bits.low -8 1
s_shr(-8, 1) = -4
$ lowentc --run u_shr bits.low 4294967288 1
u_shr(4294967288, 1) = 2147483644
$ lowentc --run ones bits.low 7
ones(7) = 3
bit_and·bit_or·bit_xor·bit_not— 비트별 논리.flip 12가 243 인 것은u8이기 때문이다. 비트 연산의 답은 언제나 타입의 폭에 매여 있다.shl·shr은 옮기기,rotl·rotr은 돌리기다.spin 129 1이 3 인 것은 맨 앞의 1 이 맨 뒤로 돌아 들어왔기 때문이고,shl이었다면 그 1 은 버려진다.shr은 부호에 따라 뜻이 다르다.−8과4294967288은 32 비트에서 같은 비트열이지만, 부호 있는 쪽은 부호 비트가 들어와 −4 가 되고 부호 없는 쪽은 0 이 들어와 2147483644 가 된다.count_ones·leading_zeros·trailing_zeros는 비트를 세고,byte_swap은 바이트 차례를 뒤집는다.clmul_lo·clmul_hi는 캐리 없는 곱셈이다. 이 둘은 기본 연산이 아니라 빌트인 연산이라call_builtin clmul_lo a. b. .로 적거나 라이브러리crypto_hw의clmul128을 부른다(32장). 캐리 없는 곱셈은 자리 올림 대신 배타합으로 더하는 곱셈이고, 암호와 검사합(GF(2^128)·CRC)이 이것 위에 선다. 답이 128 비트라 낱말 둘로 나온다. 기계에 그 명령이 있으면 처리기가 그것으로 내리고(lowentc --hw pclmul또는auto), 없으면 같은 답을 내는 셈으로 내린다 — 뜻은 하나이고 빠르기만 다르다.
옮기는 칸 수가 타입의 폭 이상이면 멈춘다. C 에서는 미정의 동작인 자리다. 칸 수를 폭 안으로 감싸기를 원하면 wrap_shl·wrap_shr 을 쓴다.
칸 수는 부호 없는 정수 타입으로 적는다. s_shr 의 n 이 u32 인 까닭이다 — i32 로 적으면 번역이 거부된다 (E-SHIFT-SIGNED). 음수 칸 수에는 정해진 뜻이 하나로 없기 때문이다. 돌리기는 칸 수를 폭으로 나눈 나머지만큼 돌리므로 어느 정수든 받는다.
문. 비트 연산은 왜 expr 섬에서 중위로 쓸 수 없는가?
답. C 에서 a & b == c 는 a & (b == c) 로 묶인다. 오래 쓴 사람도 걸리는 함정이고, 언어마다 비트 연산의 우선순위가 다르다. 섬에 넣으면 읽기 쉬워지는 대신 찾아볼 것이 는다. 전위로 적으면 괄호가 답을 말하므로 틀릴 수 없다.
4.8 부동소수점#
f32 와 f64 는 IEEE 754 의 이진 32·64 비트 형식이다. 연산은 피연산자의 폭에서 일어나고, 정수와 부동소수점 수는 서로 자동으로 바뀌지 않는다. 셈 하나는 반올림 한 번이다 — 처리기는 곱하고 더하기를 한 명령(FMA)으로 합치지 않는다. 합치면 반올림이 한 번 줄어 0.1 × 10 − 1 이 0 이 아니게 된다. 바꾸기도 반올림 한 번이다 — cast f32 는 f64 나 정수를 가장 가까운 f32 로 반올림하고, f32 에 담기지 않을 만큼 큰 수는 무한대가 된다(cast f32 <f64 1e300> 은 inf).
examples/ch04/floats.low
module floats .
rem run: third 1.0
rem run: hyp 3.0 4.0
use math .
fn third input x f64 . output f64 .
do
return div x. 3.0 . .
end .
fn hyp input a f64 . input b f64 . output f64 .
do
return math.sqrt add mul a. a. . mul b. b. . . . .
end .
실행 결과
$ lowentc --run third floats.low 1.0
third(1.0) = 0.333333
$ lowentc --run hyp floats.low 3.0 4.0
hyp(3.0, 4.0) = 5.0
VM 은 부동소수점 결과를 짧게 보여 준다(0.333333). 부동소수점 수를 0 으로 나누면 멈추지 않고 IEEE 754 가 정한 대로 무한이 된다. sqrt·sin·exp 같은 초월 함수는 표준 라이브러리 math 에 있고(use math . 뒤 math.sqrt x. .), 부동소수점 전용이며 운영체제가 있는 기계에서만 쓸 수 있다. 이 함수들의 마지막 비트가 어떤 값인지는 기계와 셈 라이브러리가 정한다.
흔한 오해. 부동소수점 수도 eq 로 비교하면 된다
math 모듈이 그 물음(close)을 준다(32장). 부동소수점 수의 반올림· NaN·−0 같은 성질은 이 언어의 증명이 다루지 않은 자리이기도 하다(50장).f32 와 크기 타입도 같은 규칙을 따른다.
examples/ch04/sizes.low
module sizes .
rem run: advance 5 -8
rem run: third32 1.0
rem run: same_third 1.0 1.0
rem usize · idiff — 길이 · 첨자와 그 차이. 폭은 구현이 정한다(16 비트 이상). 다른 정수와는 cast 로만 오간다
fn advance input n usize . input k idiff . output idiff .
requires le n. 1000 . .
requires ge k. -1000 . .
requires le k. 1000 . .
do
return add cast idiff n. . k. . .
end .
rem f32 — 32 비트 부동소수점. 메모리가 절반이고 정밀도는 약 일곱 자리
fn third32 input x f32 . output f32 . do
return div x. 3.0 . .
end .
rem 같은 1/3 도 f32 와 f64 는 다른 수다 — 넓혀서 비교하면 거짓
fn same_third input x f32 . input y f64 . output bool . do
let a f32 div x. 3.0 . .
let b f64 div y. 3.0 . .
return eq widen f64 a. . b. . .
end .
실행 결과
$ lowentc --run advance sizes.low 5 -8
advance(5, -8) = -3
$ lowentc --run third32 sizes.low 1.0
third32(1.0) = 0.333333
$ lowentc --run same_third sizes.low 1.0 1.0
same_third(1.0, 1.0) = 0
usize·idiff는 길이 · 첨자와 그 차이를 적는 부호 없는·있는 정수다. C 의size_t·ptrdiff_t에 해당한다(29장). 폭은 구현이 정하므로, 폭이 정해진 계산에는u64·i64를 쓴다. 다른 정수 타입으로는cast idiff n. .처럼cast로만 옮긴다 — 다음 절이 그 규칙이다.f32는 메모리가 절반이고 유효 숫자가 약 일곱 자리다. 큰 배열이나 그래픽처럼 정밀도보다 양이 중요한 곳에 쓴다.same_third는 같은 1/3 을f32와f64로 셈해 넓힌 뒤 비교한다. 거짓(0)이다. VM 이 둘 다0.333333으로 짧게 보여 주어도 둘은 다른 수다.
4.9 크기 타입 — usize 와 idiff#
길이 · 첨자 · 바이트 수는 usize 로 적는다. len · bitset_count · stack_count · capacity 가 내는 수가 usize 다. idiff 는 그 짝인 부호 있는 타입이고, 쓰임새는 두 위치의 차이 하나다. 이 둘은 다른 정수와 달리 폭이 이름에 없다.
4.9.1 폭은 구현이 정한다#
usize 와 idiff 의 폭은 16 비트 이상이고, 위로는 그 구현의 가장 넓은 정수 폭까지다. 이 처리기는 대상 기계의 주소 폭을 쓴다 — --target 이 정한다.
| 폭 | usize 의 가장 큰 값 | idiff 의 범위 | 그런 기계 |
|---|---|---|---|
| 16 | 65 535 | −32 768 … 32 767 | 작은 마이크로컨트롤러 — word16(번역까지만) |
| 32 | 4 294 967 295 | −2 147 483 648 … 2 147 483 647 | cortex_m 같은 32 비트 보드 |
| 64 | 264−1 | −263 … 263−1 | x86_64 · arm64 · riscv64 |
표 4.3 — 폭에 따른 usize · idiff 의 범위
같은 소스가 이 셋 어디서나 번역되도록 쓰는 것이 목표다. 그래서 아래 규칙은 폭을 묻지 않고 선다.
4.9.2 다른 정수와 섞이지 않는다 — 건너는 자리는 cast#
usize 와 idiff 는 다른 어떤 정수 타입과도 자동으로 바뀌지 않는다. u8 에서 usize 로도, usize 에서 u64 로도 그렇다. 셈과 견줌에서도 섞이지 않는다 — usize 와 u64 를 더하거나 견주려면 한쪽을 먼저 옮긴다. 옮기는 낱말은 cast 하나다.
cast u64 n. .—usize를u64로.cast usize x. .— 다른 정수를usize로.cast idiff n. .—usize를idiff로(반대도 같다).
cast 는 값이 대상 타입에 들어가면 그 값을 그대로 내고, 들어가지 않으면 멈춘다(E-VM-CAST). 조용히 자르지 않는다.
widen 과 narrow 는 usize · idiff 에 쓸 수 없다. 이 두 낱말은 방향을 말하는데, usize 에서 u32 로 가는 것이 64 비트 기계에서는 좁히기이고 16 비트 기계에서는 넓히기다. 방향이 기계에 달린 변환을 방향의 낱말로 적으면 그 글은 한 기계에서만 참이다. cast 는 방향을 묻지 않는다.
수 리터럴은 그대로 쓴다 — add i. 1 . · idx xs. 0 . 의 1 과 0 은 자리의 타입을 따른다.
examples/ch04/usize_cast.low
module usize_cast .
rem run: last [3,4,5]
rem run: bucket [1,2,3,4] 1000003
rem run: from_outside [9,9,9] 2
rem run: from_outside [9,9,9] 7
rem run: gap 3 10
rem run: packed [5,6,7,8] 3
rem 길이와 첨자는 usize 로 끝까지 들고 다닌다 --- 변환이 하나도 없다
fn last input xs slice u8 . output u8 .
requires gt len xs. . 0 . .
do
let n usize len xs. . .
return idx xs. sub n. 1 . . .
end .
rem 길이가 아닌 수(해시)와 만나는 자리 --- 넓은 쪽(u64)에서 셈하고, 첨자로 쓸 때 cast 한다
fn bucket input xs slice u8 . input h u64 . output u8 .
requires gt len xs. . 0 . .
do
let slot u64 mod h. cast u64 len xs. . . . .
return idx xs. cast usize slot. . . .
end .
rem 바깥에서 온 수는 멈추지 않는 narrow_try 로 받는다 --- 안 들어가면 none 이고 프로그램이 정한다
fn from_outside input xs slice u8 . input want u64 . output u8 . do
let p option usize narrow_try usize want. . .
guard is_some p. . else return 255 . .
let i usize some_value p. . .
guard lt i. len xs. . . else return 254 . .
return idx xs. i. . .
end .
rem 두 위치의 차이는 idiff --- 범위가 usize 의 절반이라 cast 가 지킨다
fn gap input a usize . input b usize . output idiff . do
return sub cast idiff a. . cast idiff b. . . .
end .
rem 첨자를 좁은 칸(u8)에 줄여 담는다 --- 넣을 때와 꺼낼 때 cast 를 적는다
fn packed input xs slice u8 . input i usize . output u8 .
requires lt i. len xs. . . .
do
let small u8 cast u8 i. . .
return idx xs. cast usize small. . . .
end .
실행 결과
$ lowentc --run last usize_cast.low [3,4,5]
last([3,4,5]) = 5
arg0 (written) = [3,4,5]
$ lowentc --run bucket usize_cast.low [1,2,3,4] 1000003
bucket([1,2,3,4], 1000003) = 4
arg0 (written) = [1,2,3,4]
$ lowentc --run from_outside usize_cast.low [9,9,9] 2
from_outside([9,9,9], 2) = 9
arg0 (written) = [9,9,9]
$ lowentc --run from_outside usize_cast.low [9,9,9] 7
from_outside([9,9,9], 7) = 254
arg0 (written) = [9,9,9]
$ lowentc --run gap usize_cast.low 3 10
gap(3, 10) = -7
$ lowentc --run packed usize_cast.low [5,6,7,8] 3
packed([5,6,7,8], 3) = 8
arg0 (written) = [5,6,7,8]
last는 길이를usize로 받아usize로 쓴다. 변환이 없다. 길이와 첨자를 처음부터 끝까지usize로 들고 다니면cast는 거의 나오지 않는다.bucket은 해시(u64)를 길이로 나눈 나머지를 첨자로 쓴다. 셈은 넓은 쪽에서 한다 — 길이를u64로 올려 나머지를 구하고, 그 결과를usize로 옮긴다. 나머지는 길이보다 작으므로 이cast는 어느 기계에서도 멈추지 않는다. 거꾸로 해시를 먼저usize로 옮기면 64 비트 기계에서는 돌고 32 비트 기계에서는 멈춘다.from_outside는 바깥에서 온 수를narrow_try usize로 받는다. 들어가지 않으면none이고, 프로그램이 무엇을 할지 정한다.gap은 두 위치를idiff로 옮긴 뒤 뺀다.usize끼리 빼면 결과가 0 아래로 내려갈 때 멈춘다.packed는 첨자를u8칸에 줄여 담았다가 꺼낸다. 넣을 때와 꺼낼 때cast가 한 번씩 적힌다.
4.9.3 멈추지 않고 옮기기#
cast 가 멈추면 프로그램이 끝난다. 값이 프로그램 바깥에서 왔다면 — 파일의 머리 칸, 네트워크로 받은 길이, 사용자가 친 수 — 그것이 들어가지 않는다는 것은 결함이 아니라 거절할 입력이다. 그런 자리에는 무엇을 할지를 이름이 고르는 셋을 쓴다.
| 낱말 | 하는 일 |
|---|---|
cast usize x. . | 멈춘다 — 들어가지 않는 것이 결함일 때 |
narrow_try usize x. . | option usize 를 낸다 — 들어가지 않으면 none |
narrow_sat usize x. . | 가장 큰 값에 붙인다 |
narrow_wrap usize x. . | 아랫비트만 남긴다 — 해시처럼 감기는 것이 뜻일 때만 |
표 4.4 — usize 로 옮길 때 들어가지 않으면
4.9.4 폭에 따라 조심할 것#
같은 글이 16 · 32 · 64 비트 어디서나 서게 하려면 다음을 지킨다.
- 길이가 아닌 수는 폭이 정해진 타입으로 둔다. 파일 안의 위치, 시각, 해시, 프로토콜의 칸은
u64·u32다. 32 비트 기계에도 4 GiB 를 넘는 파일이 있고, 나노초는 32 비트에 4 초 남짓밖에 담기지 않는다.usize는 메모리 안의 길이와 위치에만 쓴다. - 만나는 자리에서는 넓은 쪽으로 올려 셈한다.
add off. cast u64 len buf. . . .처럼 길이를 올린다. 좁은 쪽으로 내려 셈하면 큰 기계에서만 도는 글이 된다. usize의 셈은 그 폭에서 넘친다.mul n. size_of rec . .는 64 비트 기계에서는 넉넉해도 16 비트 기계에서는 65 535 를 넘으면 멈춘다. 틀린 값이 나오지는 않지만, 크기를 곱하는 자리에는chk_mul로 넘침을 값으로 받는 편이 낫다(앞의 「넘치면 멈춘다」).- 큰 상수는 작은 기계에서 번역되지 않는다.
usize자리의100000은 16 비트 대상에서E-TYPE-WIDTH로 거절된다. 실행 중에 멈추는 것보다 이르다. 버퍼의 크기를 상수로 적은 프로그램은 그 크기가 들어가는 기계에서만 선다. - 구조체의
usize칸은 기계마다 크기가 다르다. 64 비트에서 8 바이트, 32 비트에서 4 바이트다. 파일이나 전선에 나가는 배치에는usize를 두지 않고u32·u64로 적은 뒤 읽어 들일 때 옮긴다. idiff는usize의 절반까지만 담는다. 16 비트 기계에서 32 767 을 넘는 위치는cast idiff에서 멈춘다. 두 위치의 차이가 필요하면 큰 쪽에서 작은 쪽을usize로 빼고, 방향은 따로bool로 드는 길도 있다.- 첨자를 좁은 칸에 담을 때는 범위를 정한 것이다. 첨자를
u32칸에 담으면 원소가 232 개를 넘는 순간 넣는 쪽cast u32가 멈춘다. 그 한도는 타입을 고른 사람이 정한 것이므로requires로도 적어 둔다. - 다른 대상으로 검사해 본다.
lowentc --target cortex_m --check prog.low는usize를 32 비트로 보고 번역한다. 큰 상수와 배치의 차이가 여기서 드러난다.
반례. usize 와 다른 정수를 그대로 섞는다
examples/ch04/mistake_usizemix.low
module mistake_usizemix .
rem expect: E-TYPE-NOMINAL
rem ✘ usize 와 u64 를 그대로 더한다 --- 어느 폭에서 셈할지를 적지 않았다
fn end_of input start usize . input more u64 . output u64 . do
return add start. cast usize more. . . .
end .
실행 결과
$ lowentc --check mistake_usizemix.low
mistake_usizemix.low:6: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)
start 는 메모리 안의 위치이고 more 는 u64 다. 어느 폭에서 더할지를 글이 말하지 않았다. add cast u64 start. . more. . 로 넓은 쪽에서 더한다고 적는다. 결과를 다시 위치로 쓸 때는 cast usize 로 내린다.
반례. usize 를 widen 으로 옮긴다
examples/ch04/mistake_usizewiden.low
module mistake_usizewiden .
rem expect: E-TYPE-NOMINAL
rem ✘ usize 를 widen 으로 옮긴다 --- 넓어지는지는 기계에 달렸다. cast 로 적는다
fn as_u64 input n usize . output usize . do
return widen u64 n. . .
end .
실행 결과
$ lowentc --check mistake_usizewiden.low
mistake_usizewiden.low:6:0 E-TYPE-NOMINAL: `usize` / `idiff` convert with `cast` only — their width is the implementation's (16 bits up to the widest integer), so whether this WIDENS or NARROWS depends on the target, and `widen` promises a direction. Write `cast <type> <value>`: it asks no direction and stops when the value does not fit
mistake_usizewiden.low:6: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)
64 비트 기계만 보면 usize 에서 u64 로는 늘 넓어지는 것 같지만, 폭은 구현이 정한다. cast u64 n. . 로 적는다.
4.10 명시적 변환은 cast 가 맡는다#
값을 다른 타입으로 읽는 일은 소스에 적힌 곳에서만 일어난다. 컴파일러가 스스로 하는 변환은 넓히기 하나이고, 그 밖의 변환은 저자가 낱말로 적는다. 그 중심에 cast <타입> <값> 이 있다. 이 언어에서 명시적 변환은 cast 가 맡는다.
| 낱말 | 맡는 자리 | 값이 안 맞으면 |
|---|---|---|
widen | 값이 그대로 들어가는 넓히기 — 컴파일러도 스스로 한다 | 일어나지 않는다 |
narrow | 같은 갈래 안에서 폭을 줄인다 | 멈춘다 |
cast | 그 밖의 모든 변환 — 부호 바꾸기 · 정수와 부동소수점 사이 · usize·idiff 와 다른 정수 사이 · 참거짓에서 수로 · 열거와 수 사이 | 멈춘다 |
표 4.5 — 변환을 적는 세 낱말
examples/ch04/cast_kinds.low
module cast_kinds .
rem run: sign 200
rem run: to_float 7
rem run: to_int 7.9
rem run: bool_num 9
rem run: level_num 2
rem trap: sign 40000
rem 부호를 바꾼다 --- u32 의 값을 i16 으로 읽는다. 들어가지 않으면 멈춘다
fn sign input n u32 . output i16 . do
return cast i16 n. . .
end .
rem 갈래를 넘는다 --- 정수를 부동소수점 수로
fn to_float input n u32 . output f64 . do
return cast f64 n. . .
end .
rem 부동소수점 수를 정수로 --- 소수부는 0 쪽으로 버린다
fn to_int input x f64 . output i64 . do
return cast i64 x. . .
end .
rem 참거짓을 수로 --- 참은 1, 거짓은 0
fn bool_num input n u64 . output u8 . do
return cast u8 gt n. 5 . . .
end .
enum level do
low .
mid .
high .
end .
fn pick input k u8 . output level . do
if eq k. 0 . do return low. . end .
if eq k. 1 . do return mid. . end .
return high. .
end .
rem 열거를 수로 --- 갈래의 번호(첫 갈래 1)
fn level_num input k u8 . output u8 . do
return cast u8 pick k. . . .
end .
실행 결과
$ lowentc --run sign cast_kinds.low 200
sign(200) = 200
$ lowentc --run to_float cast_kinds.low 7
to_float(7) = 7.0
$ lowentc --run to_int cast_kinds.low 7.9
to_int(7.9) = 7
$ lowentc --run bool_num cast_kinds.low 9
bool_num(9) = 1
$ lowentc --run level_num cast_kinds.low 2
level_num(2) = 3
$ lowentc --run sign cast_kinds.low 40000
== ir diagnostics (1) ==
0:0 E-VM-CAST: value does not fit the target width (use narrow_wrap / narrow_sat / narrow_try)
sign은u32를i16으로 읽는다. 200 은 들어가서 그대로 나오고, 40000 은i16에 들어가지 않아 멈춘다.cast는 «아무렇게나 바꿔도 된다» 는 표시가 아니라 «여기서 값이 바뀔 수 있음을 안다» 는 표시다. 조용히 자르지 않는다.to_float·to_int는 갈래를 넘는다. 부동소수점 수를 정수로 바꿀 때 소수부는 0 쪽으로 버린다 — 7.9 는 7 이다.bool_num은 참거짓을 수로 바꾼다. 참은 1, 거짓은 0 이다. 거꾸로 수를 참거짓으로 바꾸는cast는 없다. 무엇을 묻는지ne n. 0 .처럼 적는다.level_num은 열거 값의 번호를 읽는다. 첫 갈래가 1 이므로high는 3 이다. 거꾸로cast level n. .은 번호가 n 인 갈래를 내고, 그런 갈래가 없으면 멈춘다. 열거 값은 수가 아니므로 이cast없이는 수 자리에 들어가지 않는다(10장).
어느 쪽으로 읽을지 갈릴 수 있는 변환은 저절로 일어나지 않는다. 같은 비트를 두 가지 수로 읽을 수 있거나, 기계에 따라 넓어지기도 하고 좁아지기도 하거나, 값의 갈래가 바뀌는 변환이 그렇다. 앞으로 언어에 들어오는 타입과 변환도 같은 규칙을 따른다 — 애매하면 cast 로 적는다.
문. cast 를 매번 적는 것은 번거롭지 않은가?
답. 적는 품은 든다. 그 대신 읽는 사람이 값이 바뀔 수 있는 자리를 모두 눈으로 찾을 수 있다. 프로그램에서 cast 를 찾아 읽으면 타입이 바뀌는 곳을 다 본 것이다. C 의 정수 승격이나 부호 섞임처럼 컴파일러가 몰래 바꾸는 자리가 하나라도 있으면 그 약속은 깨진다.
4.11 흔한 실수#
수의 실수는 번역을 통과하고 실행 중에 드러나는 것이 많다. 다른 언어라면 조용히 틀린 값이 나올 자리에서 Lowent 는 멈춘다. 멈춘 자리가 곧 고칠 자리다.
반례. 둘을 더한 뒤 반으로 나눠 가운데 값을 구한다
examples/ch04/mistake_midpoint.low
module mistake_midpoint .
rem trap: mid 200 100
fn mid input a u8 . input b u8 . output u8 .
do
rem ✘ 둘을 먼저 더하면 300 이 되어 u8 에 들어가지 않는다
return div add a. b. . 2 . .
end .
실행 결과
$ lowentc --run mid mistake_midpoint.low 200 100
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)
200 + 100 은 300 이고 u8 은 255 까지다. 나누기 전에 이미 넘친다. 이진 탐색의 (lo + hi) / 2 가 수십 년 동안 여러 라이브러리에서 터진 바로 그 결함이다. C 라면 감긴 값 44 를 반으로 나눈 22 가 조용히 나왔을 것이다. 고치는 법은 계산을 넓은 폭에서 하는 것이다.
examples/ch04/midpoint_fixed.low
module midpoint_fixed .
rem run: mid 200 100
rem run: mid 255 255
fn mid input a u8 . input b u8 . output u8 .
do
rem 한 폭 넓혀서 더한다 --- u16 은 510 까지 담는다
let both u16 add widen u16 a. . widen u16 b. . . .
rem 둘로 나눈 값은 다시 u8 에 들어간다는 것을 우리는 알고, narrow 가 그것을 확인한다
return narrow u8 div both. 2 . . .
end .
실행 결과
$ lowentc --run mid midpoint_fixed.low 200 100
mid(200, 100) = 150
$ lowentc --run mid midpoint_fixed.low 255 255
mid(255, 255) = 255
widen u16 으로 올려서 더하고, 나눈 뒤 narrow u8 로 내린다. 가운데 값은 언제나 u8 에 들어가므로 좁히기가 멈출 일은 없고, 혹시 계산을 잘못 바꾸면 그 자리에서 멈춰 알려 준다. (lo <= hi 가 보장되면 add lo. div sub hi. lo. . 2 . . 도 넘치지 않는다.)
반례. 부호 없는 수에서 큰 수를 뺀다
examples/ch04/mistake_usub.low
module mistake_usub .
rem trap: gap 3 5
fn gap input a u64 . input b u64 . output u64 .
do
rem ✘ 부호 없는 수는 0 아래로 내려가지 않는다 --- 3 − 5 는 음수가 아니라 멈춤이다
return sub a. b. . .
end .
실행 결과
$ lowentc --run gap mistake_usub.low 3 5
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)
u64 는 0 아래가 없다. 3 − 5 는 −2 가 아니라 넘침이고, 그래서 멈춘다(C 라면 18446744073709551614 가 나온다). 차이의 크기가 알고 싶으면 큰 쪽에서 작은 쪽을 뺀다 — if ge a. b. . do return sub a. b. . . end . return sub b. a. . . — 음수가 뜻이 있는 값이면 처음부터 i64 로 계산한다.
반례. 반복 변수를 너무 좁은 타입으로 둔다
examples/ch04/mistake_narrowloop.low
module mistake_narrowloop .
rem expect: E-TYPE-WIDTH
fn tally output u64 .
do
rem ✘ u8 반복 변수는 255 다음이 없다 --- 256 번째 `add i 1` 에서 멈춘다
var i u8 0 .
var n u64 0 .
while lt i. 256 . do
set n. add n. 1 . .
set i. add i. 1 . .
end .
return n. .
end .
실행 결과
$ lowentc --check mistake_narrowloop.low
mistake_narrowloop.low:9:0 E-TYPE-WIDTH: this comparison holds a value that the other side's type cannot hold, so the answer is the same for every input — the comparison decides nothing. A name declared `u8` is at most 255, `u16` at most 65535, `i8` at most 127; a literal beyond that is out of range exactly as it would be in an assignment, where the tool has always refused it. Widen the name's type, or compare against a value the type can reach. (This is how a loop written `while lt i 256 .` over a `u8` counter never ends)
mistake_narrowloop.low:9: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)
u8 은 256 에 닿을 수 없으니 lt i. 256 . 은 영원히 참이다. C 라면 i 가 255 에서 0 으로 감겨 끝나지 않는 반복이 되었을 것이다. Lowent 는 그 비교를 번역 때 E-TYPE-WIDTH 로 거절한다 — 자리에 안 들어가는 값과의 비교는 입력과 무관하게 답이 같으므로 아무것도 가르지 않는다. 반복 변수는 셀 수 있는 가장 큰 값보다 넓은 타입(u64)으로 둔다.
반례. 부동소수점 수를 정수 리터럴로 계산한다
examples/ch04/mistake_floatint.low
module mistake_floatint .
rem expect: E-TYPE-MIX
fn half input x f64 . output f64 .
do
rem ✘ f64 를 정수 리터럴 2 로 나눴다 --- 부동소수점 자리에는 2.0 을 쓴다
return div x. 2 . .
end .
실행 결과
$ lowentc --check mistake_floatint.low
mistake_floatint.low:7:0 E-TYPE-MIX: a floating-point value and an INTEGER literal are mixed in one operation. Floating point and integers do not convert implicitly (§6.2.5), so write the literal as a float (`2.0`) — or convert the other side. It used to pass `--check` and stop at run time (`E-VM-TYPE: arithmetic needs ints`)
정수와 부동소수점 수는 서로 자동으로 바뀌지 않는다. f64 를 나누려면 리터럴도 부동소수점 수로 2.0 이라고 적는다. 섞어 적으면 E-TYPE-MIX 로 번역 때 거절한다 — 전에는 실행 중에 E-VM-TYPE 으로 멈췄다.
--run 인자로 u64 의 가장 큰 값을 넘겨도 그대로 들어간다.
examples/ch04/runmax.low
module runmax .
rem run: echo 18446744073709551615
rem test
rem u64 의 위쪽 절반도 `--run` 인자로 그대로 들어간다(1.20.0 부터)
fn echo input n u64 . output u64 .
do
return n. .
end .
rem 경계값은 시험 안에서 리터럴로도 적을 수 있다
test largest_u64
do
let m u64 18446744073709551615 .
expect gt m. 9223372036854775807 . .
expect eq div m. 2 . 9223372036854775807 . .
end .
실행 결과
$ lowentc --run echo runmax.low 18446744073709551615
echo(18446744073709551615) = 18446744073709551615
$ lowentc --test runmax.low
[PASS] largest_u64
== tests: 1 run, 1 passed, 0 FAILED ==
2026-09-16 까지는 이 수가 조용히 9223372036854775807 로 잘려 들어갔고 — 다른 수를 준 것이고, 두 뒤끝이 똑같이 그랬으므로 대조로도 드러나지 않았다 — 그 뒤 1.19.0 까지는 경계에서 거절됐다(E-VM-CONTRACT). 1.20.0 부터는 0 부터 2^64 − 1 까지를 부호 없이 받는다. -1 과 2^64 는 여전히 거절된다. 경계값 시험은 largest_u64 처럼 프로그램 안의 리터럴로도 적을 수 있다.
흔한 오해. f32 와 f64 는 같은 수를 담는다
examples/ch04/mistake_f32literal.low
module mistake_f32literal .
rem run: stored
rem run: computed
rem ✘ 리터럴을 그대로 담은 f32 — 이 판에서는 32 비트로 반올림되지 않아 f64 의 0.1 과 같다고 나온다
fn stored output bool . do
let a f32 0.1 .
let b f64 0.1 .
return eq widen f64 a. . b. . .
end .
rem 셈을 한 번 거친 f32(0.1 + 0)는 32 비트로 반올림되어 다르다고 나온다
fn computed output bool . do
let a f32 0.1 .
let c f32 add a. 0.0 . .
let b f64 0.1 .
return eq widen f64 c. . b. . .
end .
실행 결과
$ lowentc --run stored mistake_f32literal.low
stored() = 0
$ lowentc --run computed mistake_f32literal.low
computed() = 0
stored 와 computed 가 둘 다 거짓이다. f32 자리에 담긴 0.1 은 32 비트로 반올림되므로, 넓혀도 f64 의 0.1 과 다른 수다. 2026-09-16 까지는 리터럴(과 --run 으로 건넨 인자)이 반올림되지 않아 stored 가 참이었다 — 같은 타입의 값이 어디서 왔느냐에 따라 다른 정밀도를 갖던 자리다. 정밀도가 중요한 코드는 폭을 적어서 고르고, 두 폭을 견줄 때는 어느 쪽으로 옮길지 widen·narrow 로 적는다.
흔한 오해. div 7 2 . 는 3.5 다
정수 나눗셈은 몫만 남기고 소수점 아래를 버린다.
examples/ch04/intdiv.low
module intdiv .
rem run: avg2 3 4
rem run: avg_f 3.0 4.0
fn avg2 input a u64 . input b u64 . output u64 .
do
rem 정수 나눗셈은 소수점 아래를 버린다 --- 7 / 2 는 3
return div add a. b. . 2 . .
end .
fn avg_f input a f64 . input b f64 . output f64 .
do
rem 소수점 아래가 필요하면 처음부터 부동소수점 수로 계산한다
return div add a. b. . 2.0 . .
end .
실행 결과
$ lowentc --run avg2 intdiv.low 3 4
avg2(3, 4) = 3
$ lowentc --run avg_f intdiv.low 3.0 4.0
avg_f(3.0, 4.0) = 3.5
avg2 3 4 는 3 이다. 소수점 아래가 필요하면 입력부터 부동소수점 수로 받아 2.0 으로 나눈다. 반올림이 필요하면 add n. div d. 2 . . 를 더한 뒤 나누는 식으로 적어서 고른다 — 언어가 대신 반올림하지 않는다.
부동소수점 수의 나머지는 정수의 mod 와 이름을 갈라 math.fmod 로 적는다.
examples/ch04/floatmod.low
module floatmod .
rem run: rest
rem run: whole_rest 7 2
use math .
rem 부동소수점 수의 나머지는 `fmod` --- 정수의 `mod` 와 이름을 가른다
fn rest output f64 .
do
return math.fmod 7.5 2.0 . .
end .
fn whole_rest input a u64 . input b u64 . output u64 .
requires gt b. 0 . .
do
return mod a. b. . .
end .
실행 결과
$ lowentc --run rest floatmod.low
rest() = 1.5
$ lowentc --run whole_rest floatmod.low 7 2
whole_rest(7, 2) = 1
math.fmod 7.5 2.0 . 은 1.5, mod 7 2 . 는 1 이다. 한 이름에 두 갈래를 싣지 않는 것은 div 와 같은 까닭이다 — 무엇을 셈하는지가 이름에서 보인다.
4.12 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
u8 … u64 · i8 … i64 | 폭이 이름에 적힌 정수 | 기계마다 크기가 달라지지 않게 |
f32 · f64 | IEEE 754 부동소수점 | 정수와 섞이지 않는다 — 리터럴도 2.0 |
widen u64 x. . | 넓히기(값을 잃지 않음) | 자동으로도 일어나지만 보이게 적을 수 있다 |
narrow u8 x. . | 좁히기 — 안 들어가면 멈춘다 | 값이 조용히 바뀌지 않게 |
narrow_sat · narrow_wrap · narrow_try | 끝값에 서기 · 감기 · option 으로 | 처분을 이름으로 고른다 |
cast i16 n. . | 그 밖의 모든 변환 — 안 들어가면 멈춘다 | 값이 바뀌는 자리가 소스에 다 보이게 |
add · sub · mul | 넘치면 멈추는 산술 | 넘침은 기본이 결함 |
wrap_add · sat_add · chk_add | 감김 · 포화 · option | 의도한 처분이 소스에 남는다 |
div · mod | 몫(0 쪽으로 버림) · 나머지(나누는 수의 부호) | 0 나누기와 MIN / −1 은 멈춘다 |
nonzero_of b. . · div_nz | 0 이 아님을 한 번 확인해 타입에 싣기 | 검사를 한 자리로 옮긴다 |
bit_and · bit_or · bit_xor · bit_not | 비트별 논리 | 기호 우선순위의 함정이 없다 |
shl · shr · rotl · rotr | 옮기기 · 돌리기 | 폭 이상 옮기면 멈춘다 |
call_builtin clmul_lo · call_builtin clmul_hi | 캐리 없는 곱셈(빌트인 연산) — 답은 낱말 둘 | 기계 명령이 있으면 그것으로, 없으면 셈으로 |
call_builtin mul_hi(bigint64.mul_hi) | u64 곱의 위쪽 64 비트(빌트인 연산) — 아래쪽은 wrap_mul | C 컴파일러에 128 비트 정수가 있으면 곱 하나, 없으면 32 비트 곱 넷으로 |
count_ones · leading_zeros · trailing_zeros · byte_swap | 비트 세기 · 바이트 뒤집기 | 흔한 일은 이름 하나로 |
true · false · and · or · not | 참거짓과 그 논리 | 수를 조건에 쓰지 않는다 |
usize · idiff · f32 | 길이 · 첨자의 정수(폭은 구현이 정한다) · 32 비트 부동소수점 | 길이와 위치를 적을 때 · 정밀도보다 양이 중요할 때 |
cast usize x · cast u64 n | usize · idiff 와 다른 정수 사이를 옮긴다 | 폭이 기계마다 달라도 같은 뜻이게 |
표 4.6 — 수의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
narrow 로 적는다. 넘침·0 나누기·범위 밖 좁히기·폭 이상의 옮기기는 멈추며, 다른 처분(감김·포화·값으로)은 wrap_·sat_·chk_·narrow_try 같은 이름으로 고른다. mod 의 부호는 나누는 수를 따른다. 참거짓은 수가 아니고, 비트 연산은 낱말로 적으며 폭과 부호가 뜻을 정한다.