Lowent 매뉴얼←↑→

7 흐름 — 갈래, 되풀이, 빠져나가기

먼저 알아야 할 것

4장 수 · 참거짓은 수가 아니다
6장 지역 · if 는 값을 내지 않는 문장이다

돌아보기

6장에서 let x u64 be if gt a 1 . 5 else 6 . 은 왜 거절되었고, 갈래마다 값을 정하려면 어떻게 적는가?

답. if 는 값을 내지 않는 문장이라서 식의 자리에 둘 수 없다(E-IF-VALUE). 갈래마다 값을 정하려면 var 를 기본값으로 짓고 갈래 안에서 set 하거나, 갈래마다 return 한다. 이 장은 그 갈래 자체와, 되풀이와 빠져나가기의 규칙을 다룬다.

이 장의 필요성과 맥락

흐름의 문장은 어느 언어에나 있어서 새로 배울 것이 없어 보인다. 그러나 Lowent 의 흐름 문장에는 검사가 붙어 있다. 조건은 참거짓이어야 하고, guard 의 else 는 반드시 떠나야 하며, 값을 돌려주는 op 은 모든 길에서 돌려주어야 하고, match 는 모든 경우를 덮어야 한다. 이 검사들은 “어떤 길로 가면 값이 없다” 는 결함을 번역할 때 없앤다. 데이터(제3부)로 넘어가기 전에 흐름의 약속을 먼저 세운다.

이 장이 끝나면

if … else, while, for, break·continue 를 쓰는 법을 익힌다. guard 가 조건을 그 아래 코드에 대한 사실로 바꾸는 방식과, else 가 떠나지 않으면 왜 거절되는지 알게 된다. 모든 길이 값을 돌려주어야 한다는 규칙, 수와 범위를 가르는 match 와 그 망라 검사, 그리고 panic 이 효과라는 것도 보게 된다.

이 장에서 답할 질문

  1. for 로 0 부터 n 까지 세는 반복은 어떻게 적는가?
  2. match 가 if 사슬보다 나은 점은 망라 검사 말고 무엇인가?

7.1 조건과 되풀이#

if 는 조건이 참일 때 블록을 실행하고, else 로 거짓일 때의 블록을 적는다. while 은 조건이 참인 동안 되풀이하고, for 는 슬라이스의 원소를 차례로 훑는다. 조건은 모두 bool 이어야 한다.

examples/ch07/loops.low

module loops .
rem run: count_big 10
rem run: first_zero [5,3,0,9]
rem run: first_zero [5,3]
rem run: odd_sum [1,2,3,4,5]

fn count_big input n u32 . output u32 .
  requires le n 100 .
do
  var total u32 be 0 .
  var i u32 be 0 .
  while lt i n . do
    if gt i 5 . do
      set total (add total 1) .
    end
    set i (add i 1) .
  end
  return total .
end

fn first_zero input xs slice u8 . output u64 .
do
  var i u64 be 0 .
  while lt i (len xs) . do
    if eq (index xs i) 0 . do
      break .
    end
    set i (add i 1) .
  end
  return i .
end

fn odd_sum input xs slice u8 . output u64 .
do
  var acc u64 be 0 .
  for x xs do
    if eq (mod x 2) 0 . do
      continue .
    end
    set acc (add acc (widen u64 x)) .
  end
  return acc .
end

실행 결과

$ lowentc --run count_big loops.low 10
count_big(10) = 4
$ lowentc --run first_zero loops.low [5,3,0,9]
first_zero([5,3,0,9]) = 2
  arg0 (written) = [5,3,0,9]
$ lowentc --run first_zero loops.low [5,3]
first_zero([5,3]) = 2
  arg0 (written) = [5,3]
$ lowentc --run odd_sum loops.low [1,2,3,4,5]
odd_sum([1,2,3,4,5]) = 9
  arg0 (written) = [1,2,3,4,5]

for x in xs 처럼 in 을 적으면 E-VOCAB-REMOVED 로 거절된다. 훑을 대상은 이름 바로 뒤에 온다.

문. for 로 0 부터 n 까지 세는 반복은 어떻게 적는가?

답. for 는 슬라이스를 훑는 도구라서 수의 범위를 직접 받지 않는다. 수를 세는 반복은 while 과 var 로 적는다. 세는 반복을 한 가지 모양으로만 적게 하면, 반복 변수의 범위를 컴파일러가 알아보기 쉽고 경계 검사를 지우는 분석도 단순해진다. 슬라이스를 걸러 세거나 모으는 일은 pipe(24장)가 맡는다.

7.2 guard — 조건을 사실로 바꾼다#

guard <조건> . else <빠져나감> . 은 조건이 참이 아니면 그 자리에서 떠난다. 떠나는 문장은 return·break·continue·panic 이다.

examples/ch07/guards.low

module guards .
rem run: head_or_zero [7,8]
rem run: head_or_zero []
rem run: grade 95
rem run: grade 42

fn head_or_zero input data slice u8 . output u8 .
do
  guard ge (len data) 1 . else return 0 .
  return index data 0 .
end

fn grade input score u8 . output u8 .
do
  guard le score 100 . else return 0 .
  if ge score 90 . do
    return 65 .
  end else do
    if ge score 60 . do
      return 66 .
    end
  end
  return 70 .
end

실행 결과

$ lowentc --run head_or_zero guards.low [7,8]
head_or_zero([7,8]) = 7
  arg0 (written) = [7,8]
$ lowentc --run head_or_zero guards.low []
head_or_zero([]) = 0
  arg0 (written) = []
$ lowentc --run grade guards.low 95
grade(95) = 65
$ lowentc --run grade guards.low 42
grade(42) = 70

head_or_zero 의 guard 를 지난 코드는 슬라이스가 비지 않은 세계에서만 산다. 그래서 index data 0 이 안전하다. grade 는 guard 로 범위 밖을 먼저 걸러 내고, 그 아래에서 if … end else do … end 로 갈래를 나눈다.

guard 가 if not 의 다른 이름이 아닌 까닭은 else 가 반드시 떠나야 한다는 데 있다. 떠나지 않으면 거절된다.

examples/ch07/fallthrough.low

module fallthrough .
rem expect: E-GUARD-FALLTHROUGH

fn clamp5 input n u32 . output u32 .
do
  guard le n 5 . else do
    let capped u32 be 5 .
  end
  return n .
end

실행 결과

$ lowentc --check fallthrough.low
fallthrough.low:6:0 E-GUARD-FALLTHROUGH: this `guard`'s `else` FALLS THROUGH — it must LEAVE (`return` / `break` / `continue` / `panic`). If it falls through, the code AFTER the guard wrongly believes the condition holds. Leaving is the WHOLE point of `guard`: it is what lets the rest of the op assume the condition. (SPEC-002 D6 always said so and nothing enforced it — which made `guard` an exact synonym of `if not`.) If you do not mean to leave, write `if`

else 가 떠나지 않고 아래로 흘러내리면, guard 아래 코드는 n 이 5 이하라고 믿지만 실제로는 그렇지 않다. 믿을 수 없는 사실을 믿게 두느니 번역을 거절한다. 떠나지 않을 일이면 if 를 쓴다.

흔한 오해. guard 는 코드를 짧게 하는 문법 설탕이다

짧아지는 것은 부수 효과다. guard 의 본뜻은 컴파일러와 읽는 사람에게 사실 하나를 건네는 것이다. 컴파일러는 guard 를 지난 뒤 그 조건을 참으로 알고 경계 검사를 지우는 데 쓴다. 같은 뜻을 if … do return … end 로 적어도 동작은 같지만, 떠남이 문법으로 보장되지는 않는다.

7.3 모든 길이 값을 돌려준다#

값을 돌려주는 op 은 모든 길에서 값을 돌려주고 끝나야 한다.

examples/ch07/partial.low

module partial .
rem expect: E-RETURN-PARTIAL

fn sign_flag input a i32 . output u8 .
do
  if gt a 0 . do
    return 1 .
  end
end

실행 결과

$ lowentc --check partial.low
partial.low:4:0 E-RETURN-PARTIAL: this op says it OUTPUTS a value, but some path through its body reaches the end without a `return`. Until now the tool quietly returned 0 there — a value that appears NOWHERE in your source (RFC-0019 G-TOTAL). Give every path a `return`, or say `output void` if it really produces nothing. An exhaustive `match` whose every arm returns counts as returning

a 가 0 이하인 길에는 return 이 없다. 옛 도구는 그 길에서 조용히 0 을 돌려주었다. 소스 어디에도 없는 값이다. 지금은 거절된다. 값을 돌려주고 끝나는 것으로 인정되는 문장은 return, 두 갈래가 모두 돌려주는 if … else, 모든 갈래가 돌려주는 match 다. while·for·guard 는 빠져나가는 길이 있으므로 그 자체로 돌려주는 것으로 보지 않는다. 그래서 반복 뒤에는 늘 return 이 온다.

돌려줄 값이 없는 op(output void)은 return 없이 몸의 끝까지 가도 된다. 끝나는 자리가 곧 돌아가는 자리다.

7.4 match — 경우를 빠짐없이#

match 는 값을 경우별로 가른다. if 사슬과 다른 점은 모든 경우를 다뤄야 한다는 것이다.

examples/ch07/bands.low

module bands .
rem run: band 9
rem run: band 10
rem run: band 200
rem run: half 128
rem run: big 101

fn band input x u8 . output u8 .
do
  match x do
    case 0 to 9 . do return 1 . end
    case 10 to 19 . do return 2 . end
    case _ . do return 0 . end
  end
end

fn half input b u8 . output u8 .
do
  match b do
    case 0 to 127 . do return 0 . end
    case 128 to 255 . do return 1 . end
  end
end

fn big input x u8 . output u8 .
do
  match x do
    case y when gt y 100 . do return 1 . end
    case _ . do return 0 . end
  end
end

실행 결과

$ lowentc --run band bands.low 9
band(9) = 1
$ lowentc --run band bands.low 10
band(10) = 2
$ lowentc --run band bands.low 200
band(200) = 0
$ lowentc --run half bands.low 128
half(128) = 1
$ lowentc --run big bands.low 101
big(101) = 1

빈틈이 있으면 거절된다.

examples/ch07/inexhaustive.low

module inexhaustive .
rem expect: E-MATCH-INEXHAUSTIVE

fn band input x u8 . output u8 .
do
  match x do
    case 0 to 9 . do return 1 . end
    case 11 to 255 . do return 2 . end
  end
end

실행 결과

$ lowentc --check inexhaustive.low
inexhaustive.low:6:0 E-MATCH-INEXHAUSTIVE: this `match` on an integer is not exhaustive — the literals/ranges leave a gap, so a `case _ .` wildcard (or `else`) is required, OR the ranges must TILE the whole domain of the scrutinee's type (e.g. `0 to 127` + `128 to 255` on a u8) (RFC-0020 §6.4; MM4 tiling)

10 이 어느 갈래에도 들지 않는다. 같은 경우를 두 번 적거나 _ 뒤에 갈래를 두면 E-MATCH-REDUNDANT 로 거절된다. 경고가 아니라 오류다 — 한 번도 실행될 수 없는 갈래는 대개 결함을 숨긴다.

문. match 가 if 사슬보다 나은 점은 망라 검사 말고 무엇인가?

답. 나중에 경우가 늘어날 때 드러난다. 열거형에 변형을 하나 더하면, 그 열거형을 가르는 모든 match 가 번역에서 멈춰 고쳐야 할 자리를 알려 준다(10장). if 사슬은 새 경우를 조용히 마지막 else 로 흘려보낸다. 또 --ir 로 보면 도구가 갈래 수와 최악 비교 횟수 같은 디스패치 비용을 말해 준다.

7.5 panic 은 효과다#

panic 은 프로그램을 즉시 멈춘다. 되돌릴 수 없는 멈춤이므로 효과이고, 쓰는 op 은 effects panic 을 적는 proc 이어야 한다.

examples/ch07/panics.low

module panics .
rem run: checked 3
rem trap: checked 0

proc checked input n u32 . output u32 . effects panic .
do
  if eq n 0 . do
    panic "n must not be zero" .
  end
  return div 100 n .
end

실행 결과

$ lowentc --run checked panics.low 3
checked(3) = 33
$ lowentc --run checked panics.low 0
== ir diagnostics (1) ==
0:0 E-VM-PANIC: the program called `panic` — this is an unrecoverable stop, and it is NOT a contract violation (the code chose to stop, it did not break a promise)

VM 은 E-VM-PANIC 을 내며, 이것이 계약 위반이 아니라는 것을 분명히 한다 — 코드가 멈추기로 고른 것이지 약속을 어긴 것이 아니다. 순수한 fn 은 panic 을 쓸 수 없다. 다만 넘침이나 계약 위반으로 처리기가 일으키는 멈춤은 fn 에서도 일어난다. 그것은 op 이 한 일이 아니라 처리기가 약속을 지키게 한 일이다.

panic 은 복구할 수 없는 상황에만 쓴다. 호출자가 다룰 수 있는 실패는 값으로 돌려준다 (11장). 둘을 가르는 기준은 17장이 다룬다.

7.6 흔한 실수#

반례. for x in xs 로 적는다

examples/ch07/mistake_forin.low

module mistake_forin .
rem expect: E-VOCAB-REMOVED

fn total input xs slice u8 . output u64 .
do
  var t u64 be 0 .
  rem ✘ `in` 을 적었다 --- `for` 는 `for <이름> <슬라이스> do` 다
  for x in xs do
    set t (add t (widen u64 x)) .
  end
  return t .
end

실행 결과

$ lowentc --check mistake_forin.low
8:0 E-VOCAB-REMOVED: `in` is gone. In a loop write `for <name> <slice> do` — the slice follows the name and `do` marks the body, so `in` carried nothing. As access, `b in a` was a reverse spelling of `field a b`: write `field a b` / `index a i` (the glued dot `a.b` is refused too, `E-FIELD-GLUED`)

for 는 for <이름> <슬라이스> do 다. 훑을 대상이 이름 바로 뒤에 오고, 몸의 시작은 do 가 알리므로 in 이 전할 것이 없다. 같은 뜻에 표시를 하나 더 두지 않으려고 없앴다. 고치는 법: for x xs do.

반례. elif 로 갈래를 잇는다

examples/ch07/mistake_elif.low

module mistake_elif .
rem expect: E-BLOCK-NOHEAD

fn grade input n u64 . output u64 .
do
  if lt n 50 . do
    return 0 .
  rem ✘ `elif` 는 낱말이 아니다 --- 이어지는 갈래는 `end else if … do` 다
  end elif lt n 80 . do
    return 1 .
  end
  return 2 .
end

실행 결과

$ lowentc --check mistake_elif.low
9:22 E-BLOCK-NOHEAD: a `do … end` block needs a head that owns it — `if … do`, `while … do`, `fn … do`, `make T do`, `region … do` … A bare block is not in the grammar; it used to pass `--check` and then could not be lowered. Put its statements where they belong, or give it its head

elif·elsif·else if: 는 언어마다 다르다. Lowent 는 이미 있는 낱말을 이어 붙인다 — 앞 블록을 end 로 닫고 else if 를 붙인다. elif 는 낱말이 아니므로 elif lt n 80 . 이 따로 선 문장으로 읽히고, 그 뒤의 do … end 는 여는 머리가 없는 블록이 된다(E-BLOCK-NOHEAD) — do … end 는 늘 if·while·fn 같은 머리가 연다.

examples/ch07/elseif.low

module elseif .
rem run: grade 30
rem run: grade 70
rem run: grade 95

fn grade input n u64 . output u64 .
do
  if lt n 50 . do
    return 0 .
  rem 갈래를 이을 때는 앞 블록을 `end` 로 닫고 `else if` 를 붙인다
  end else if lt n 80 . do
    return 1 .
  end else do
    return 2 .
  end
end

실행 결과

$ lowentc --run grade elseif.low 30
grade(30) = 0
$ lowentc --run grade elseif.low 70
grade(70) = 1
$ lowentc --run grade elseif.low 95
grade(95) = 2

마지막 갈래는 end else do … end 이다. 갈래가 셋 이상이고 모두 한 값을 가르는 것이면 match 가 더 알맞다.

반례. C 처럼 블록 안에 else 를 둔다

examples/ch07/mistake_innerelse.low

module mistake_innerelse .
rem expect: E-STMT-ELSE

fn pick input a u8 . output u8 .
do
  var r u8 be 0 .
  if gt a 5 . do
    set r 1 .
  rem ✘ C 처럼 블록 안에 `else` 를 한 줄로 두었다 --- `end else do` 로 앞 블록을 닫아야 한다
  else
    set r 2 .
  end
  return r .
end

실행 결과

$ lowentc --check mistake_innerelse.low
mistake_innerelse.low:10:0 E-STMT-ELSE: `else` sits INSIDE the block, the way C writes it. Here a block is closed before the other arm opens: `if <cond> . do … end else do … end`. Written this way the arm used to be accepted by every static check and then dropped at lowering — the VM stopped with an unsupported body and the native build silently left the op out

C 나 몇몇 언어는 if … { … } else { … } 처럼 else 가 앞 블록 뒤에 붙는다. Lowent 에서 else 는 앞 블록을 end 로 닫은 뒤에 온다(end else do). 블록 안에 else 를 한 줄로 두면 E-STMT-ELSE 로 거절한다 — 전에는 번역이 통과하고 실행에서야 E-VM-UNSUP 으로 멈췄으며, 네이티브 빌드는 그 op 을 아예 뺐다.

반례. 조건에 < 같은 기호를 쓴다

examples/ch07/mistake_less.low

module mistake_less .
rem expect: E-CHAR

fn upto input n u64 . output u64 .
do
  var i u64 be 0 .
  rem ✘ `<` 는 이 언어의 기호가 아니다 --- 비교는 `lt i n` 이다
  while i < n . do
    set i (add i 1) .
  end
  return i .
end

실행 결과

$ lowentc --check mistake_less.low
8:11 E-CHAR: unexpected character
8:11 E-FORM-UNEXPECTED: unexpected token in form

비교는 낱말이다 — lt(작다)·le(작거나 같다)·gt·ge·eq·ne. < 는 이 언어가 모르는 글자라 E-CHAR 가 난다. 기호의 우선순위를 외우지 않아도 되게 하려는 선택이고, 긴 산술에는 expr 섬이 있다(8장). 고치는 법: while lt i n . do.

반례. continue 가 증가를 건너뛴다

while 로 세면서 몸 가운데서 continue 하면, 그 아래에 둔 set i (add i 1) . 도 함께 건너뛴다.

fn odd_count input xs slice u8 . output u64 .
do
  var n u64 be 0 .
  var i u64 be 0 .
  while lt i (len xs) . do
    if eq (mod (index xs i) 2) 0 . do
      continue .
    end
    set n (add n 1) .
    set i (add i 1) .
  end
  return n .
end

첫 짝수에서 i 가 더 이상 오르지 않아 반복이 끝나지 않는다. 번역도 실행 검사도 이것을 잡지 못한다 — 멈추지 않는 것은 넘침이 아니기 때문이다. 증가를 몸의 맨 앞으로 옮기거나(그러면 색인에는 증가 전의 값을 따로 담는다), 원소를 훑는 일이면 처음부터 for x xs do 를 쓴다. for 는 다음 원소로 넘어가는 일을 언어가 맡으므로 이 결함이 생길 자리가 없다.

나머지를 받는 자리는 두 가지로 적을 수 있다. case _ . 와 마지막 else 는 같은 일을 한다.

examples/ch07/matchelse.low

module matchelse .
rem run: band 1
rem run: band 3
rem run: band 99

fn band input n u64 . output u64 . do
  match n do
    case 1 do
      return 10 .
    end
    case 2 to 5 do
      return 20 .
    end
    rem 나머지를 `else` 로 받는다 --- `case _ .` 와 같은 자리다
    else do
      return 99 .
    end
  end
end

실행 결과

$ lowentc --run band matchelse.low 1
band(1) = 10
$ lowentc --run band matchelse.low 3
band(3) = 20
$ lowentc --run band matchelse.low 99
band(99) = 99

else 뒤에는 갈래를 더 둘 수 없다 — 이미 다 받았으므로 그 뒤의 갈래는 한 번도 돌지 않고, E-MATCH-REDUNDANT 로 거절된다.

흔한 오해. match 의 갈래도 C 의 switch 처럼 아래로 흘러간다

examples/ch07/nofall.low

module nofall .
rem run: label 1
rem run: label 2
rem run: label 9

fn label input c u64 . output u64 .
do
  var score u64 be 0 .
  match c do
    rem 맞은 갈래 하나만 실행된다 --- 다음 갈래로 흘러가지 않으므로 `break` 가 필요 없다
    case 1 . set score (add score 10) .
    case 2 . set score (add score 20) .
    case _ . set score (add score 1) .
  end
  return score .
end

실행 결과

$ lowentc --run label nofall.low 1
label(1) = 10
$ lowentc --run label nofall.low 2
label(2) = 20
$ lowentc --run label nofall.low 9
label(9) = 1

맞은 갈래 하나만 실행되고 다음 갈래로 흘러가지 않는다. break 를 적는 버릇도, 빠뜨려서 생기는 결함도 없다. 두 경우에 같은 일을 하려면 갈래를 or 로 묶는다(11장).

7.7 이 장의 문법 한눈에#

모양뜻왜 이렇게
if c . do … end조건이 참이면 블록조건도 폼이라 마침표로 닫고, 몸은 do 로 연다
if c . do … end else do … end둘 중 하나여는 말과 닫는 말이 늘 짝을 이룬다
… end else if c2 . do … end갈래 잇기새 낱말(elif) 없이 있는 낱말을 잇는다
while c . do … end조건이 참인 동안수를 세는 반복은 이 모양 하나
for x xs do … end슬라이스의 원소를 차례로다음 원소로 넘어가는 일을 언어가 맡는다
break . · continue .반복에서 나가기 · 다음 회차로흐름을 바꾸는 문장
guard c . else return … .조건이 아니면 떠난다 — 지난 뒤엔 조건이 사실else 가 반드시 떠나야 한다
return e .값을 돌려주고 끝낸다값을 내는 op 은 모든 길에서
match v do case … . 문장 … end경우별로 가르기빠짐없이, 겹침 없이 — 흘러내림이 없다
case 1 to 9 . · case _ . · case y when c .범위 · 나머지 전부 · 가드가 붙은 갈래정수는 경우가 많아 _ 가 흔히 필요하다
panic "…" .되돌릴 수 없는 멈춤(효과)effects panic 을 적는 proc 에서만

표 7.1 — 흐름의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나

복습 정리

조건은 bool 이어야 한다. while 은 조건으로, for 는 슬라이스로 되풀이하고 break·continue 로 흐름을 바꾼다. guard 의 else 는 반드시 떠나고, 지난 뒤에는 조건이 사실이 된다. 값을 돌려주는 op 은 모든 길에서 돌려주어야 한다. match 는 빠짐없이, 겹침 없이 경우를 덮어야 한다. panic 은 효과다.