Lowent 매뉴얼←↑→

8 식 — 전위 표기와 expr 섬

먼저 알아야 할 것

3장 겉모습 · 이름이 먼저, 인자가 뒤
4장 수 · 비트 연산은 낱말로 적는다
7장 흐름 · 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.) . 로 적으라고 했다. 이 장은 그 섬 안의 규칙 전체를 다룬다.

이 장의 필요성과 맥락

식은 가장 많이 쓰는 문법이다. Lowent 의 식은 기본이 전위 표기이고 중위 표기는 expr 섬 안에서만 허락된다. 이 두 겹 구조를 제대로 알아야 점이 줄줄이 이어진 전위 식을 읽을 수 있고, 섬 안에서 무엇이 허락되지 않는지를 알아 헤매지 않는다. 제2부의 마지막 장으로, 앞의 장들에서 조금씩 본 식의 규칙을 한곳에 모은다.

이 장이 끝나면

전위 표기에 우선순위가 없는 이유와, expr 섬의 짧은 우선순위표를 알게 된다. 섬 안에서도 op 부름은 제 점으로 닫힌다는 것과 단항 연산자가 없다는 것, 비교와 and·or 를 섞을 때 괄호로 묶는다는 것을 익힌다. and·or 의 단락 평가로 안전한 조건을 적는 법과, 번역할 때 계산되는 size_of·comptime 도 보게 된다.

이 장에서 답할 질문

  1. 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
2eq ne lt le gt ge이어 쓸 수 없다같은 이름의 전위 연산
1band왼쪽and(단락 평가)
1aor왼쪽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 은 번역할 때 계산된다.