8 식 — 전위 표기와 expr 섬
먼저 알아야 할 것
match 는 모든 경우를 덮어야 한다돌아보기
3장은 expr a. lt b. lt c. . 를 왜 거절했는가? 진단은 무엇으로 고치라고 했는가?
답. 비교를 이어 쓰면 수학에서는 “a < b 이고 b < c” 로 읽히지만 많은 언어에서는 (a < b) < c 로 읽힌다. 같은 글자가 사람과 기계에게 다른 뜻이 되므로 E-EXPR-CHAIN 으로 거절했다. 진단은 expr (a. lt b.) and (b. lt c.) . 로 적으라고 했다. 이 장은 그 섬 안의 규칙 전체를 다룬다.
이 장의 필요성과 맥락
expr 섬 안에서만 허락된다. 이 두 겹 구조를 제대로 알아야 점이 줄줄이 이어진 전위 식을 읽을 수 있고, 섬 안에서 무엇이 허락되지 않는지를 알아 헤매지 않는다. 제2부의 마지막 장으로, 앞의 장들에서 조금씩 본 식의 규칙을 한곳에 모은다.이 장이 끝나면
expr 섬의 짧은 우선순위표를 알게 된다. 섬 안에서도 op 부름은 제 점으로 닫힌다는 것과 단항 연산자가 없다는 것, 비교와 and·or 를 섞을 때 괄호로 묶는다는 것을 익힌다. and·or 의 단락 평가로 안전한 조건을 적는 법과, 번역할 때 계산되는 size_of·comptime 도 보게 된다.이 장에서 답할 질문
- VM 은
in_range(5, 1, 9) = 1이라고 보여 준다.bool이 수가 된 것인가?
8.1 전위 표기에는 외울 것이 없다#
전위 표기에서 연산은 이름이 먼저 오고 인자가 뒤에 온다. 이름이 폼을 열고 점이 닫는다. 이 모양에는 우선순위가 없다. 무엇이 먼저 계산되는지는 점이 전부 말한다 — 안쪽 폼이 먼저 닫힌다.
examples/ch08/island.low
module island .
rem run: score 3 4
rem run: score_prefix 3 4
rem run: grouped 3 4
rem run: in_range 5 1 9
rem run: spread 3 9
fn score input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return expr a. + b. * 2 . .
end .
fn score_prefix input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return add a. mul b. 2 . . .
end .
fn grouped input a u32 . input b u32 . output u32 .
requires le a. 1000 . .
requires le b. 1000 . .
do
return expr (a. + b.) * 2 . .
end .
fn in_range input x u32 . input lo u32 . input hi u32 . output bool .
do
return expr (lo. le x.) and (x. le hi.) . .
end .
fn spread input a u32 . input b u32 . output u32 .
do
return expr (max a. b. .) - (min a. b. .) . .
end .
실행 결과
$ lowentc --run score island.low 3 4
score(3, 4) = 11
$ lowentc --run score_prefix island.low 3 4
score_prefix(3, 4) = 11
$ lowentc --run grouped island.low 3 4
grouped(3, 4) = 14
$ lowentc --run in_range island.low 5 1 9
in_range(5, 1, 9) = 1
$ lowentc --run spread island.low 3 9
spread(3, 9) = 6
score_prefix 의 add a. mul b. 2 . . 와 score 의 expr a. + b. * 2 . 는 같은 식이다. 두 op 의 답이 같고, 섬은 전위로 번역되므로 실행 비용도 같다. 섬은 표기의 투영일 뿐 다른 연산이 아니다.
기호 대신 낱말을 쓰는 이유도 같은 곳에 있다. ^ 는 어떤 언어에서 거듭제곱이고 어떤 언어에서 배타적 논리합이다. bit_xor 는 어디서 읽어도 한 뜻이다. 그리고 낱말은 소리 내어 읽을 수 있다.
8.2 expr 섬의 우선순위표#
섬 안에서만 우선순위가 있고, 표는 이것이 전부다.
| 단계 | 연산 | 결합 | 같은 뜻의 전위 |
|---|---|---|---|
| 5 | ( ) 묶음 | — | 묶음 표시일 뿐 값이 아니다 |
| 4 | * / | 왼쪽 | mul · div |
| 3 | + - | 왼쪽 | add · sub |
| 2 | eq ne lt le gt ge | 이어 쓸 수 없다 | 같은 이름의 전위 연산 |
| 1b | and | 왼쪽 | and(단락 평가) |
| 1a | or | 왼쪽 | or(단락 평가) |
표 8.1 — expr 섬 안의 우선순위(강한 것이 위)
grouped 의 expr (a. + b.) * 2 . 처럼 괄호로 차례를 바꿀 수 있다. 표에 없는 연산 — 나머지 mod, 비트 연산, min·max — 은 섬 안에서도 전위로 적는다. spread 의 expr (max a. b. .) - (min a. b. .) . 가 그 모양이다. 전위 폼은 제 점으로 닫히므로 여기서 괄호는 꾸밈이다.
in_range 는 비교 두 개를 and 로 묶었다. 표로는 비교가 and 보다 강하므로 expr lo. le x. and x. le hi. . 처럼 괄호 없이 적어도 뜻은 같다(이 장의 “흔한 실수” 에 그 예가 있다). 그래도 비교를 and·or 와 섞을 때는 비교마다 괄호로 묶는다. 읽는 사람도 표를 떠올릴 필요가 없어진다.
문. VM 은 in_range(5, 1, 9) = 1 이라고 보여 준다. bool 이 수가 된 것인가?
답. 아니다. VM 의 결과 줄이 bool 을 0 과 1 로 보여 줄 뿐이다. 프로그램 안에서 bool 은 수와 섞이지 않는다(4장). 네이티브로 내보낸 C 에서도 bool 은 한 바이트의 0 또는 1 로 표현된다.
8.3 섬 안에서 하지 못하는 것#
섬 안에서도 op 을 부를 수 있다. 부름은 이름이 열고 제 점이 닫으므로, twice 가 어디까지인지는 점이 말한다.
examples/ch08/app.low
module app .
rem run: bump 3
rem run: bump_marked 3
fn twice input a u32 . output u32 .
requires le a. 1000 . .
do
return mul a. 2 . .
end .
fn bump input a u32 . output u32 .
requires le a. 100 . .
do
return expr twice a. . + 1 . .
end .
fn bump_marked input a u32 . output u32 .
requires le a. 100 . .
do
rem 괄호는 꾸밈이다 --- 같은 7 이다
return expr (twice a. .) + 1 . .
end .
실행 결과
$ lowentc --run bump app.low 3
bump(3) = 7
$ lowentc --run bump_marked app.low 3
bump_marked(3) = 7
expr twice a. . + 1 . 에서 a. 뒤의 점이 twice 를 닫고, 마지막 점이 섬을 닫는다. 눈에 띄게 하려면 bump_marked 처럼 괄호로 감싸도 된다 — 뜻은 같다.
섬 안은 중위 연산자의 세계다. 그래서 없는 것이 하나 있다. 단항 연산자다. 섬에는 왼쪽 피연산자가 없는 연산자가 없다.
examples/ch08/unary_bad.low
module unary_bad .
rem expect: E-EXPR-UNARY
fn flip input a i32 . output i32 .
do
return expr - a. . .
end .
실행 결과
$ lowentc --check unary_bad.low
unary_bad.low:6:0 E-EXPR-UNARY: an `expr` island has INFIX operators only — there is no unary form. `expr - 5` has nothing on the left of `-`. Write the prefix op instead (`neg 5`, `not b`, `bit_not x`), or give the left operand. The island exists to read like arithmetic; a unary sign would make `a - 5` and `a (- 5)` two readings of the same letters (SPEC-002 §2.5 · RFC-0091)
부호를 뒤집으려면 neg a. . 를 전위로 적거나, 섬 안이라면 expr 0 - a. . 로 적는다. not b. . · bit_not x. . 도 같다.
흔한 오해. 섬이 작은 것은 아직 덜 만들어서다
8.4 단락 평가로 조건을 지킨다#
and 와 or 는 앞쪽만으로 답이 정해지면 뒤쪽을 계산하지 않는다. 이것을 이용하면 뒤쪽 식이 안전하게 계산될 조건을 앞쪽에 둘 수 있다.
examples/ch08/shortcircuit.low
module shortcircuit .
rem run: safe_first_is_zero []
rem run: safe_first_is_zero [0,4]
rem run: ratio_ok 0 0
rem run: ratio_ok 10 5
fn safe_first_is_zero input xs slice u8 . output bool .
do
return and gt len xs. . 0 . eq idx xs. 0 . 0 . . .
end .
fn ratio_ok input num u32 . input den u32 . output bool .
do
return and ne den. 0 . ge div num. den. . 2 . . .
end .
실행 결과
$ lowentc --run safe_first_is_zero shortcircuit.low []
safe_first_is_zero([]) = 0
arg0 (written) = []
$ lowentc --run safe_first_is_zero shortcircuit.low [0,4]
safe_first_is_zero([0,4]) = 1
arg0 (written) = [0,4]
$ lowentc --run ratio_ok shortcircuit.low 0 0
ratio_ok(0, 0) = 0
$ lowentc --run ratio_ok shortcircuit.low 10 5
ratio_ok(10, 5) = 1
safe_first_is_zero 는 빈 슬라이스에서 idx xs. 0 . 을 계산하지 않는다. 앞쪽 gt len xs. . 0 . 이 거짓이면 거기서 답이 거짓으로 정해지기 때문이다. ratio_ok 도 분모가 0 이면 나눗셈을 하지 않는다. 이 모양은 guard 를 쓰기엔 작은 조건에 알맞다. 조건이 op 전체에 대한 사실이라면 guard 가 낫다 (7장).
8.5 번역할 때 계산되는 식#
어떤 식은 실행하기 전에 값이 정해진다.
examples/ch08/compt.low
module compt .
rem run: table_size
rem run: pick
fn table_size output usize .
do
return mul size_of u32 . 16 . .
end .
fn pick output u32 .
do
match comptime add 2 3 . do
case 0 do return 100 . end .
case 5 do return 500 . end .
case _ do return 999 . end .
end .
end .
실행 결과
$ lowentc --run table_size compt.low
table_size() = 64
$ lowentc --run pick compt.low
pick() = 500
size_of u32 . 는 그 타입 값 하나의 바이트 수를 번역할 때 준다. 타입을 매개변수로 받는 코드가 가장 넓은 폭을 가정하지 않고 비용을 정직하게 계산할 수 있게 한다(22장).
comptime add 2 3 . 은 식을 번역할 때 계산하라는 표시다. match 의 가를 값이 번역 시점 상수이면 match 는 맞는 갈래 하나로 접히고 실행 중 비교는 사라진다. 그래도 접힌 다른 갈래들은 타입 검사를 받는다. C 의 #ifdef 로 지운 코드가 검사도 받지 않는 것과 다르다. 빌드 설정을 읽는 config <옵션> 도 번역 시점 상수로 쓰인다(31장).
comptime 식은 이름도 읽는다. 모듈 상수, 값이 번역 시점에 나온 지역 let, machine.*, native_lanes <타입>, config <옵션> 이다. 지역 let 의 값이 번역 시점에 나온다는 것은 그 값을 리터럴, 이런 이름 하나, native_lanes <타입>, comptime <식> 가운데 하나로 적었다는 뜻이다. var 와 입력은 읽지 않는다. 모듈 상수의 값도 comptime <식> 으로 적을 수 있다.
examples/ch08/compt_names.low
module compt_names .
rem run: page_bytes
rem run: lanes_twice
rem 모듈 상수는 `comptime` 식으로 적을 수 있다
let KIB u64 1024 .
let PAGE u64 comptime mul 4 KIB. . . .
fn page_bytes output u64 .
do
let extra u64 4 .
rem 상수 · 값이 번역 시점에 나온 `let` 을 읽는다
return comptime add PAGE. extra. . . .
end .
fn lanes_twice output usize .
do
rem 대상에게 묻는 값도 셈에 쓴다
return comptime mul native_lanes u32 . 2 . . .
end .
실행 결과
$ lowentc --run page_bytes compt_names.low
page_bytes() = 4100
$ lowentc --run lanes_twice compt_names.low
lanes_twice() = 8
셈한 답은 상수다. 받는 자리는 그 답을 리터럴처럼 재므로, 자리에 들지 않으면 return 600 . 과 같이 거절된다.
examples/ch08/compt_width.low
module compt_width .
rem expect: E-TYPE-WIDTH
let K u64 6 .
fn f output u8 .
do
rem ✘ 셈한 답 600 은 `u8` 에 들지 않는다 — `return 600 .` 과 같다
return comptime mul K. 100 . . .
end .
실행 결과
$ lowentc --check compt_width.low
compt_width.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)
순수한 fn 을 부르는 것도 comptime 이 셈한다. 처리기는 번역할 때 그 fn 을 실제로 돌리고, 실행할 때에는 답인 수 하나만 남는다. 지금은 입력과 답이 모두 정수나 bool 하나씩인 fn 만 돌린다. 효과를 낼 수 있는 proc 은 돌리지 않는다. 돌리다가 넘치거나 계약을 어기거나 정해진 걸음 안에 끝나지 않으면 E-COMPTIME-NONCONST 로 거절되고, 메시지가 까닭을 말한다.
examples/ch08/compt_fn.low
module compt_fn .
rem run: table_len
rem run: small
rem 순수한 `fn` 은 번역할 때 돌 수 있다
fn fact input n u64 . output u64 .
do
var r u64 1 .
var i u64 1 .
while le i. n. . do
set r. mul r. i. . .
set i. add i. 1 . .
end .
return r. .
end .
rem 모듈 상수를 그 답으로 적는다
let PERMS u64 comptime fact 6 . . .
fn table_len output u64 .
do
return PERMS. .
end .
fn small output u8 .
do
rem 답 120 은 상수라서 `u8` 자리에 든다
return comptime fact 5 . . .
end .
실행 결과
$ lowentc --run table_len compt_fn.low
table_len() = 720
$ lowentc --run small compt_fn.low
small() = 120
배열을 내는 fn(9장)도 번역할 때 돌린다. 원소는 크기 있는 수다. 돌린 답은 프로그램에 박힌 읽기 전용 표가 되어, 실행할 때에는 표를 짓는 일이 없다. 답에는 let x array <길이> <타입> comptime f … . 처럼 그 fn 의 결과와 길이와 원소 타입이 같은 이름을 붙인다. 몸 안의 let 이어도 되고 모듈 상수여도 된다. 이 표는 읽기만 한다 — 원소를 쓰거나(set · copy · swap) mut 입력에 넘기면 거절된다. 고쳐 쓸 사본이 필요하면 lit array <길이> <타입> _ . 로 지은 var 에 copy 한다.
examples/ch08/compt_table.low
module compt_table .
rem run: sq7
rem run: cube_sum
rem 배열을 내는 `fn` 도 번역할 때 돈다
fn squares output r array 16 u16 .
do
var i usize 0 .
while lt i. 16 . do
let v u16 cast u16 i. . .
set idx r. i. . mul v. v. . .
set i. add i. 1 . .
end .
return r. .
end .
fn cubes input k u32 . output r array 4 u32 .
do
var i usize 0 .
while lt i. 4 . do
let v u32 add k. cast u32 i. . . .
set idx r. i. . mul mul v. v. . v. . .
set i. add i. 1 . .
end .
return r. .
end .
rem 모듈 상수 — 프로그램에 박힌 읽기 전용 표 하나
let SQ array 16 u16 comptime squares . . .
fn sq7 output u16 .
do
return idx SQ. 7 . .
end .
fn cube_sum output u32 .
do
rem 몸 안의 `let` 도 된다 — 2³+3³+4³+5³
let t array 4 u32 comptime cubes 2 . . .
return add add idx t. 0 . idx t. 1 . . add idx t. 2 . idx t. 3 . . . .
end .
실행 결과
$ lowentc --run sq7 compt_table.low
sq7() = 49
$ lowentc --run cube_sum compt_table.low
cube_sum() = 224
배열의 원소로는 bool 과 줄(배열 안의 배열)도 된다. 슬라이스를 내는 fn 과 구조체를 내는 fn 도 번역할 때 돈다. 슬라이스 답은 배열 답과 같이 읽기 전용 바이트가 되고, 구조체 답은 그 구조체의 값이 된다 — 칸마다 돌린 값을 적은 lit 과 같다. 입력으로는 정수와 bool 말고도 바이트가 번역 시점에 정해진 슬라이스를 건넬 수 있다: 따옴표 문자열, text 리터럴, 원소가 모두 상수인 목록, 배열이나 슬라이스 모듈 상수다.
examples/ch08/compt_values.low
module compt_values .
rem run: hello_len
rem run: origin_y
rem run: vowels
rem 슬라이스를 내는 `fn` — 입력의 한 조각을 돌려준다
fn trim input s slice u8 . output slice u8 .
do
var a usize 0 .
var b usize len s. . .
while and lt a. b. . eq idx s. a. . 32 . . do set a. add a. 1 . . end .
while and gt b. a. . eq idx s. sub b. 1 . . 32 . . do set b. sub b. 1 . . end .
return subslice s. a. b. . .
end .
struct pt do
x i32 .
y i32 .
end .
rem 구조체를 내는 `fn`
fn corner input k i32 . output pt .
requires le k. 1000 . .
requires ge k. -1000 . .
do
return lit pt do x k. . y neg k. . . end . .
end .
rem 바이트를 세는 `fn` — `text` 리터럴을 입력으로 받는다
fn count_vowels input s slice u8 . output u64 .
do
var n u64 0 .
for c s. do
if or or eq c. 97 . eq c. 101 . . or or eq c. 105 . eq c. 111 . . eq c. 117 . . . do set n. add n. 1 . . end .
end .
return n. .
end .
let HELLO slice u8 comptime trim " hello " . . .
let ORIGIN pt comptime corner 3 . . .
fn hello_len output usize .
do
return len HELLO. . .
end .
fn origin_y output i32 .
do
return field ORIGIN. y . .
end .
fn vowels output u64 .
do
return comptime count_vowels text END
a quiet morning
END
. . .
end .
실행 결과
$ lowentc --run hello_len compt_values.low
hello_len() = 5
$ lowentc --run origin_y compt_values.low
origin_y() = -3
$ lowentc --run vowels compt_values.low
vowels() = 6
구조체도 건넬 수 있다. 칸마다 상수를 적은 lit 이나 번역 때 돈 fn 의 구조체 답이면 된다(구조체는 바이트 배치가 있어야 한다 — 슬라이스 · 배열 칸이 없는 것). 구조체의 슬라이스도 입력과 답이 된다. 번역 때 도는 fn 은 새 메모리를 짓지 않으므로, 슬라이스 답은 입력의 조각이거나 상수다. 128 비트 칸을 가진 구조체 답은 위 절반까지 그대로 남는다.
examples/ch08/compt_structs.low
module compt_structs .
rem run: scaled
rem run: rest_x
struct pt do
x u16 .
y u16 .
end .
rem 구조체를 받는 `fn` — 번역 때 도는 `fn` 에 구조체를 건넨다
fn scale input p pt . input k u16 . output pt .
do
return lit pt do x mul field p. x . k. . . y mul field p. y . k. . . end . .
end .
rem 구조체의 슬라이스를 받아 그 조각을 돌려준다 — 새 저장소를 짓지 않는다
fn from input ps slice pt . input k usize . output slice pt .
do
return subslice ps. k. len ps. . . .
end .
let P pt comptime scale lit pt do x 3 . y 4 . end . 10 . . .
let REST slice pt comptime from lit slice pt lit pt do x 1 . y 2 . end . lit pt do x 5 . y 6 . end . lit pt do x 7 . y 8 . end . . 1 . . .
rem 30 + 40
fn scaled output u16 .
do
return add field P. x . field P. y . . .
end .
rem 조각의 첫 원소 — 5
fn rest_x output u16 .
do
return field idx REST. 0 . x . .
end .
실행 결과
$ lowentc --run scaled compt_structs.low
scaled() = 70
$ lowentc --run rest_x compt_structs.low
rest_x() = 5
번역 때 도는 fn 은 순수하므로 새 메모리를 짓지 않는다. 그래도 표는 지을 수 있다 — 답의 자리는 바깥이 주고(배열 결과의 자리), 답 안의 슬라이스 칸은 입력 글의 조각을 가리키면 된다. 처리기는 그 조각을 읽기 전용 바이트로 두고 칸이 그곳을 가리키게 한다. 슬라이스 칸이 있는 구조체도 목록과 배열 결과의 원소가 된다(칸은 주소와 길이 두 낱말이다). 줄 수를 미리 모르면 자리를 넉넉히 잡고, 채운 수는 따로 센다. 자리가 모자라면 칸 쓰기가 범위 밖에서 멈추고, 그 상수의 선언에서 번역이 거절된다.
examples/ch08/compt_messages.low
module compt_messages .
rem run: lines
rem run: thanks_len
struct msg do
ko slice u8 .
en slice u8 .
end .
rem 줄마다 `ko,en` 을 쪼갠다 — 슬라이스 칸은 입력 글의 조각이다
fn parse input txt slice u8 . output r array 8 msg .
do
var row usize 0 .
var start usize 0 .
var comma usize 0 .
var i usize 0 .
while le i. len txt. . . do
if or eq i. len txt. . . eq idx txt. i. . 10 . . do
if gt i. start. . do
set idx r. row. . lit msg do ko subslice txt. start. comma. . . en subslice txt. add comma. 1 . i. . . end . .
set row. add row. 1 . .
end .
set start. add i. 1 . .
end else do
if eq idx txt. i. . 44 . do set comma. i. . end .
end .
set i. add i. 1 . .
end .
return r. .
end .
fn count_lines input txt slice u8 . output usize .
do
var n usize 0 .
for c txt. do if eq c. 10 . do set n. add n. 1 . . end . end .
return add n. 1 . .
end .
fn same input s slice u8 . output slice u8 .
do
return s. .
end .
let RAW slice u8 comptime same text END
안녕,hello
고마워,thanks
END
. . .
rem 자리는 넉넉히 8 — 채운 줄 수는 따로 센다
let N usize comptime count_lines RAW. . . .
let MSGS array 8 msg comptime parse RAW. . . .
fn lines output usize .
do
return N. .
end .
fn thanks_len output usize .
do
return len field idx MSGS. 1 . en . . .
end .
실행 결과
$ lowentc --run lines compt_messages.low
lines() = 2
$ lowentc --run thanks_len compt_messages.low
thanks_len() = 6
표의 원문을 소스에 적지 않고 파일로 두려면 include_text "<경로>" 를 쓴다. 번역할 때 그 파일을 읽어 바이트(slice u8)를 번역 때 도는 fn 에 건넨다. 경로는 이 소스 파일이 있는 폴더에서 보므로 어디서 지어도 같은 파일을 읽는다. 그 폴더 밖(절대 경로나 .., 그리고 링크를 따라가 닿는 바깥)은 거절된다. include_text 는 번역 때 도는 fn 의 입력 자리에만 둔다 — 다른 자리에 두면 실행할 때 파일을 읽는 것처럼 보이므로 E-INCLUDE-PLACE 로 거절된다. 파일을 고치면 간직한 답 대신 fn 이 다시 돈다. 잠금 파일(pkg.lock)이 있는 빌드에서는 읽은 파일도 의존처럼 내용으로 고정된다 — lowentc --lock-write 가 include:<경로> 항목을 적고, 그 뒤 파일이 바뀌면 E-DEP-HASH 로 멈춘다(31장).
examples/ch08/compt_include.low
module compt_include .
rem run: words
rem run: longest
rem 줄 수를 센다
fn count_lines input txt slice u8 . output usize .
do
var n usize 0 .
for c txt. do if eq c. 10 . do set n. add n. 1 . . end . end .
return n. .
end .
rem 가장 긴 줄의 길이
fn longest_line input txt slice u8 . output usize .
do
var best usize 0 .
var cur usize 0 .
for c txt. do
if eq c. 10 . do
if gt cur. best. . do set best. cur. . end .
set cur. 0 .
end else do set cur. add cur. 1 . . end .
end .
return best. .
end .
rem 이 파일 옆의 `compt_words.txt` 를 번역할 때 읽는다
let WORDS usize comptime count_lines include_text "compt_words.txt" . . . .
let LONGEST usize comptime longest_line include_text "compt_words.txt" . . . .
fn words output usize .
do
return WORDS. .
end .
fn longest output usize .
do
return LONGEST. .
end .
실행 결과
$ lowentc --run words compt_include.low
words() = 3
$ lowentc --run longest compt_include.low
longest() = 6
번역 때 도는 fn 하나는 걸음 100만 안에 끝나야 한다. 더 오래 걸리는 표라면 pkg.low 에 build comptime_steps 50000000 . 처럼 적거나 명령줄에 --comptime-steps 50000000 을 준다(명령줄이 이긴다). 환경 변수 LOWENT_CTFE_CACHE 에 폴더를 주면 돌린 답을 그 폴더에 간직해 다음 번역에서 다시 쓴다. fn 의 정의나 입력이 바뀌었거나 처리기 판 · 대상 · 걸음 예산이 다르면 간직한 답을 쓰지 않는다.
8.6 흔한 실수#
비교(단계 2)가 and(단계 1b)보다 강하므로, 괄호 없이 섞어도 뜻은 하나다.
examples/ch08/mixcmp.low
module mixcmp .
rem run: in_range 3 1 5
rem run: in_range 9 1 5
fn in_range input x u64 . input lo u64 . input hi u64 . output bool .
do
rem 비교가 `and` 보다 강하므로 괄호 없이 섞어도 뜻이 하나다
return expr lo. le x. and x. le hi. . .
end .
실행 결과
$ lowentc --run in_range mixcmp.low 3 1 5
in_range(3, 1, 5) = 1
$ lowentc --run in_range mixcmp.low 9 1 5
in_range(9, 1, 5) = 0
그래도 괄호를 적으면 읽는 사람이 표를 떠올리지 않아도 된다. 아래가 같은 일을 괄호로 적은 것이다.
examples/ch08/mixcmp_fixed.low
module mixcmp_fixed .
rem run: in_range 5 1 9
rem run: in_range 12 1 9
fn in_range input x u64 . input lo u64 . input hi u64 . output bool .
do
return expr (lo. le x.) and (x. le hi.) . .
end .
실행 결과
$ lowentc --run in_range mixcmp_fixed.low 5 1 9
in_range(5, 1, 9) = 1
$ lowentc --run in_range mixcmp_fixed.low 12 1 9
in_range(12, 1, 9) = 0
반례. 나머지를 % 로 적는다
examples/ch08/mistake_percent.low
module mistake_percent .
rem expect: E-CHAR
fn last_digit input a u64 . output u64 .
do
rem ✘ `%` 는 섬의 연산자가 아니다 --- 나머지는 `mod` 를 괄호로 부른다
return expr a. % 10 . .
end .
실행 결과
$ lowentc --check mistake_percent.low
7:18 E-CHAR: unexpected character
섬의 연산자는 표에 있는 것이 전부다. % 는 C 에서는 나머지지만 언어마다 부호 규칙이 달라(음수의 나머지) 같은 기호가 다른 답을 낸다. Lowent 의 나머지는 이름 mod 하나이고 부호는 나누는 수를 따른다(4장). 섬 안에서도 전위로 부른다.
examples/ch08/percent_fixed.low
module percent_fixed .
rem run: last_digit_plus 1234 1
fn last_digit_plus input a u64 . input k u64 . output u64 .
do
rem 표에 없는 연산은 섬 안에서도 전위로 적고 괄호로 감싼다
return expr (mod a. 10 .) + k. . .
end .
실행 결과
$ lowentc --run last_digit_plus percent_fixed.low 1234 1
last_digit_plus(1234, 1) = 5
반례. 거듭제곱을 ^ 로 적거나 정수에 math.pow 를 쓴다
^ 는 섬의 연산자가 아니다(E-CHAR) — 어떤 언어는 거듭제곱, 어떤 언어는 배타적 논리합이라서 넣지 않았다. 정수의 제곱은 mul a. a. . 로 적는다. 그리고 math.pow 는 부동소수점의 거듭제곱이다 — 두 입력이 f64 다.
examples/ch08/mistake_pow.low
module mistake_pow .
rem expect: E-TYPE-ARG
use math .
fn square input a u64 . output u64 .
do
rem ✘ 정수에 `pow` 를 썼다 — 초월 함수는 부동소수점 전용이다
return math.pow a. 2 . .
end .
실행 결과
$ lowentc --check mistake_pow.low
mistake_pow.low:9:0 E-TYPE-ARG: call argument type does not match parameter
mistake_pow.low:9:0 E-TYPE-ARG: call argument type does not match parameter
mistake_pow.low:9:0 E-TYPE-RETURN: the returned value does not match the op's `output` — expected `u64`, found `f64`
정본은 pow·sqrt·sin·cos·exp·log·fmod 를 부동소수점 전용으로 정한다(§6.3.9). math 의 op 은 입력을 f64 로 적어 두었으므로, 정수를 주면 인자의 타입이 맞지 않아 E-TYPE-ARG 로 거절된다. 2026-09-16 까지는 거절하지 않고 square 5 가 25 가 아니라 5 를 냈다 — 번역도 실행도 알리지 않는 조용한 오답이었다. 정수의 거듭제곱은 곱셈으로 적고, 부동소수점 수로 셈하려면 cast f64 n. . 으로 먼저 옮긴다.
examples/ch08/pow_fixed.low
module pow_fixed .
rem run: square_f 1.5
rem run: square_i 5
use math .
fn square_f input a f64 . output f64 .
do
rem `pow` 는 부동소수점에서 맞게 돈다
return math.pow a. 2.0 . .
end .
fn square_i input a u64 . output u64 .
requires le a. 1000 . .
do
rem 정수의 거듭제곱은 곱셈으로 적는다 — 이름이 하는 일을 말한다
return mul a. a. . .
end .
실행 결과
$ lowentc --run square_f pow_fixed.low 1.5
square_f(1.5) = 2.25
$ lowentc --run square_i pow_fixed.low 5
square_i(5) = 25
흔한 오해. 중첩된 부름에는 괄호가 반드시 있어야 한다
examples/ch08/noparen.low
module noparen .
rem run: with_parens 3 4
rem run: without_parens 3 4
fn with_parens input a u64 . input b u64 . output u64 .
do
return add 1 (mul a. b. .) . .
end .
fn without_parens input a u64 . input b u64 . output u64 .
do
rem 괄호가 없어도 `mul` 은 제 점에서 닫힌다 --- 같은 13 이다
return add 1 mul a. b. . . .
end .
실행 결과
$ lowentc --run with_parens noparen.low 3 4
with_parens(3, 4) = 13
$ lowentc --run without_parens noparen.low 3 4
without_parens(3, 4) = 13
괄호는 꾸밈이다. 안쪽 부름이 어디서 끝나는지는 그 부름의 점이 정한다. 그래서 add 1 mul a. b. . . 와 add 1 (mul a. b. .) . 는 같은 식이다. 1.7 판까지는 각 op 이 받는 인자 수가 끝을 정했고, 그래서 식을 읽으려면 op 의 선언을 알아야 했다. 지금은 점만 보면 된다 — 긴 식에서 묶음을 눈에 띄게 하고 싶을 때만 괄호를 적는다.
8.7 이 장의 문법을 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
add a. mul b. c. . . | 전위 표기 — 우선순위가 없다 | 점이 계산 차례를 다 말한다 |
expr a. + b. * c. . | 중위 섬 — * / 가 + - 보다 강하다 | 학교에서 배운 셈과 같은 것만 중위로 |
expr (a. + b.) * c. . | 섬 안의 묶음 | 괄호는 값이 아니라 묶음 표시 |
expr (lo. le x.) and (x. le hi.) . | 비교를 논리와 섞기 | 비교마다 괄호 — 표를 떠올리지 않아도 된다 |
expr twice a. . + 1 . | 섬 안에서 op 부르기 | 부름은 제 점으로 닫힌다 — 괄호는 꾸밈 |
expr mod a. 10 . + k. . | 표에 없는 연산(mod·비트·min) | 섬은 커지지 않는다 — 전위로 부른다 |
expr 0 - a. . · neg a. . | 부호 뒤집기 | 섬에 단항 연산자가 없다(E-EXPR-UNARY) |
and · or | 단락 평가 — 앞쪽으로 답이 정해지면 뒤를 계산하지 않는다 | 뒤쪽 식이 안전할 조건을 앞에 둔다 |
size_of u32 . | 타입 하나의 바이트 수(번역할 때) | 제네릭 코드가 비용을 정직하게 센다 |
comptime add 2 3 . | 번역할 때 계산 | 접힌 갈래도 타입 검사는 받는다 |
표 8.2 — 식의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
expr 섬은 사칙·비교·and·or 만 중위로 허락하고, 섬 안의 op 부름도 제 점으로 닫히며 단항 연산자는 없다. 비교를 and·or 와 섞을 때는 비교마다 괄호로 묶는다. and·or 의 단락 평가로 뒤쪽 식을 지킬 수 있고, size_of 와 comptime 은 번역할 때 계산된다.