10 제어

구문
control = "if" { body } [ "else" { body } ] "end"
        | "loop" [ "(" NAME ")" ] { body } "end"
        | "case" { branch } [ "otherwise" { body } "end" ] "end" ;
branch  = values "of" { body } "end" ;
values  = case-value | "(" case-value { "," case-value } ")" ;
leave   = ( "while" | "until" | "break" ) [ "(" NAME ")" ] | "return" ;
block   = "do" [ "(" effect ")" ] { body } "end" ;

(1) end 는 가장 안쪽에 열린 것 하나를 닫는다. 여는 낱말은 word test record do if loop case of otherwise or_else defer 다. (텍스트 리터럴과 note 주석은 자기 꼬리표로 닫는다, 6.3, 6.5.)

(2) 처리기는 적재 때 모든 갈래의 스택을 견준다. 갈래가 다시 만나는 자리에서 스택은 어느 갈래로 왔든 같아야 한다. 떠나는 갈래(leaving branch) — return fail break 로 끝나거나 끝나지 않는 루프로 끝나는 갈래 — 는 견주지 않는다.

10.1 갈림

(1) 조건 if 몸 end 는 맨 위의 bool 을 내려 참이면 몸을 돈다. 조건 if 몸 else 몸 end 는 거짓이면 else 의 몸을 돈다.

(2) else 가 없는 if 의 몸은 스택을 들어올 때와 같게 둬야 한다. else 가 있으면 두 몸은 같은 스택을 남겨야 한다.

10-if.pnx

use io
word sign (n int gives str) n 0 lt if "negative" else n 0 eq if "zero" else "positive" end end end
-5 sign io.println
0 sign io.println
word warn (n int) n 100 gt if "big" io.println end end      rem no else: the branch leaves the stack as it was
500 warn

출력

$ pnx 10-if.pnx
negative
zero
big
제약

(3) 두 갈래가 남기는 스택이 다르면 거절된다.

10-if-unequal.pnx

word f (n int gives int) n 0 lt if 1 2 else 3 end end

거절

$ pnx 10-if-unequal.pnx
10-if-unequal.pnx:1:47: the two branches of this if leave different stacks

10.2 루프

(1) loop 몸 end 는 몸을 되풀이한다. 몸 안에서 다음 낱말이 루프를 떠난다.

셋은 몇 번이든 쓸 수 있고, 루프의 몸에 바로 쓰거나 그 안의 갈래(if else case or_else) 안에 쓴다. 가장 안쪽 루프를 떠난다.

(2) 한 바퀴는 스택을 들어올 때와 같게 둬야 한다. 떠나는 자리마다 스택은 루프에 들어올 때와 같아야 한다. 루프의 결과가 필요하면 지역 변수에 둔다.

10-loop.pnx

use io
use str
word digits (n int gives int)            rem how many decimal digits
  1 to count_
  loop
    n 10 div to n
    n 0 eq until
    count_ 1 add to count_
  end
  count_
end
12345 digits str.to_str io.println
word first_square_over (limit int gives int)
  1 to i
  loop
    i i mul limit gt if break end
    i 1 add to i
  end
  i
end
50 first_square_over str.to_str io.println

출력

$ pnx 10-loop.pnx
5
8

(3) loop (이름) 은 루프에 이름을 붙인다. while (이름) until (이름) break (이름) 은 그 이름의 루프까지 한 번에 떠난다. 이름은 그 루프의 몸 안에서만 보이고 값이 아니다. 가림 금지(9.5)를 따른다.

10-loop-named.pnx

use io
use str
word find_pair (target int gives str)      rem the first a, b in 1..9 with a*b = target
  1 to a
  0 to b
  loop (rows)
    1 to b
    loop
      a b mul target eq until (rows)       rem leaves both loops at once
      b 1 add to b
      b 9 gt until
    end
    a 1 add to a
    a 9 gt if "none" return end
  end
  cat (a, " x ", b)
end
42 find_pair io.println

출력

$ pnx 10-loop-named.pnx
6 x 7

(4) 자기를 떠나는 while until break 가 하나도 없는 루프는 끝나지 않는 루프다. 끝나지 않는 루프는 떠나는 갈래로 센다. 그래서 그 뒤에 결과를 적지 않아도 낱말의 서명이 맞는다. 안쪽 루프의 until 은 바깥 루프를 떠나는 것으로 세지 않지만, 안쪽에 쓴 until (바깥 이름) 은 센다.

10-forever.pnx

use io
use str
word first_multiple (n int, k int gives int)     rem a loop with no exit counts as leaving
  loop
    n k mod 0 eq if n return end
    n 1 add to n
  end
end
100 7 first_multiple str.to_str io.println

출력

$ pnx 10-forever.pnx
105
제약

(5) 한 바퀴가 스택을 바꾸면 거절된다.

10-loop-net.pnx

word f (gives int)
  0
  loop
    1 add
    dup 10 gt until
    1
  end
end

거절

$ pnx 10-loop-net.pnx
10-loop-net.pnx:7:3: one turn of a loop must leave the stack as it found it

(6) while until break 를 루프 밖에 쓰면 거절된다. do 블록 안에서 바깥 루프를 떠나지도 못한다. each, each_index, times 의 블록 안에서는 그 블록을 떠난다(10.6 (4)).

10-while-outside.pnx

word f (n int) n 0 gt while end

거절

$ pnx 10-while-outside.pnx
10-while-outside.pnx:1:23: while, until and break belong inside a loop

(7) 떠나는 낱말(return fail break)이나 끝나지 않는 루프 바로 뒤에 같은 갈래의 코드가 더 있으면 닿지 않는 코드로 거절된다.

10-unreachable.pnx

word f (n int gives int) n return 1 add end

거절

$ pnx 10-unreachable.pnx
10-unreachable.pnx:1:35: this code cannot be reached: the code before it always leaves

10.3 고르기

(1) 값 case 가지… end 는 맨 위의 int, 좁은 정수(7.6), err 나 str 를 내려 가지의 값과 견주고, 같은 첫 가지의 몸을 돈다. 가지의 값은 리터럴, 이 소스의 const, 오류 코드(err.range), 또는 다른 모듈이 내보낸 상수나 실패 코드(모듈.이름, 8.14)다. 한 가지에 값이 여럿이면 괄호에 쉼표로 적는다. 맞는 가지가 없으면 otherwise 의 몸을 돈다. 가지마다 같은 스택을 남겨야 한다.

10-case.pnx

use io
const quit_code 9
word describe (cmd str gives str)
  cmd case
    "add" of "adds a line" end
    ("rm", "del") of "deletes a line" end
    otherwise "unknown command" end
  end
end
word level (n int gives str)
  n case
    0 of "off" end
    1 of "low" end
    quit_code of "quit" end
    otherwise "high" end
  end
end
"del" describe io.println
"mv" describe io.println
9 level io.println

출력

$ pnx 10-case.pnx
deletes a line
unknown command
quit
제약

(2) 같은 값이 두 가지에 있으면 거절된다.

10-case-twice.pnx

word f (n int gives int) n case 1 of 10 end 1 of 20 end otherwise 0 end end end

거절

$ pnx 10-case-twice.pnx
10-case-twice.pnx:1:45: this value is already in an earlier branch of the case

(3) 모듈의 상수(json.key, err.full 같은 경로)는 가지의 값이 되지 못한다. 그런 값은 eq 로 견준다.

10-case-path.pnx

use json
word f (k int gives bool) k case json.key of true end otherwise false end end end

거절

$ pnx 10-case-path.pnx
10-case-path.pnx:2:34: a case value is a literal or the name of a const
결함

(4) otherwise 가 없고 맞는 가지도 없으면 결함 range 다.

10-case-nomatch.pnx

use io
word f (n int gives str) n case 1 of "one" end 2 of "two" end end end
2 f io.println
3 f io.println

결함으로 멈춤

$ pnx 10-case-nomatch.pnx
two
10-case-nomatch.pnx:2: index out of range
  n int = 3

10.4 돌아가기

(1) return 은 낱말을 그 자리에서 끝낸다. 그 자리의 스택은 결과와 맞아야 한다(9.2 (4)).

제약

(2) return 은 정의(낱말, 값이 되는 블록, 시험) 안에서만 쓴다. 정의 밖의 코드에 쓰면 거절된다.

10-return-outside.pnx

use io
"start" io.println
return

거절

$ pnx 10-return-outside.pnx
10-return-outside.pnx:3:1: return belongs inside a definition

10.5 블록

(1) do … end 는 블록(block)이다. 블록은 두 가지로 쓰인다. 모양은 같고, 블록을 받는 쪽이 가른다.

(2) do 바로 뒤에 서명을 쓸 수 있고, 그 이름들은 낱말 정의와 같이 블록의 지역 변수가 된다. 받는 쪽이 효과를 정해 주면 서명을 줄일 수 있다.

10-inline-locals.pnx

use io
use str
word total_over (limit int gives int)
  0 to acc
  1 10 range do (i int) i limit gt if acc i add to acc end end each   rem the block sees acc and limit
  acc
end
5 total_over str.to_str io.println

출력

$ pnx 10-inline-locals.pnx
30

10-value-block.pnx

use io
use str
word twice (n int, f word (int gives int) gives int) n f call f call end
5 do (x int gives int) x 3 mul end twice str.to_str io.println
word add_one (int gives int) 1 add end
word demo (gives int)
  do (x int gives int) x add_one end to step     rem a block value in a local
  41 step call
end
demo str.to_str io.println

출력

$ pnx 10-value-block.pnx
45
42

(2a) 서명이 없는 값이 되는 블록의 몸이 word 로 정의한 낱말(이 소스의 것이든 쓰는 모듈이 내보낸 것이든) 하나 뿐이면(do by_len end, do geo.area end), 그 값은 그 낱말 자체다. 타입은 그 낱말의 서명을 가진 word 이고, call 은 그 낱말을 부른다. 다른 낱말은 블록의 서명을 쓴 블록으로 준다. 타입만 쓴 입력은 스택에 남으므로(9.2 (2)) 블록의 몸은 그 낱말 하나다: do (str, str gives bool) str.starts_with end.

10-word-value.pnx

use io
use str
word by_len (a str, b str gives bool) a str.len b str.len lt end
word shorter (f word (str, str gives bool), a str, b str gives str) a b f call if a else b end end
word demo ()
  do by_len end to order                          rem the word by_len itself: word (str, str gives bool)
  cat ("shorter: ", order "pear" "fig" shorter) io.println
  do (str, str gives bool) str.starts_with end to starts    rem a word of the language, with its signature
  "banana" "ban" starts call if "starts with ban" io.println end
end
demo

출력

$ pnx 10-word-value.pnx
shorter: fig
starts with ban
제약

(3) 값이 되는 블록이 바깥의 지역 변수를 쓰면 거절된다(그 이름은 블록 안에 선언돼 있지 않다).

10-value-block-outer.pnx

word twice (n int, f word (int gives int) gives int) n f call f call end
word scale (k int gives int) 5 do (x int gives int) x k mul end twice end

거절

$ pnx 10-value-block-outer.pnx
10-value-block-outer.pnx:2:55: this name is not declared before this place

(4) 루프로 펼쳐지는 블록(each times 등)은 스택을 들어올 때와 같게 둬야 한다.

10-block-net.pnx

array xs int 3
word f (gives int) 0 xs do (x int) x end each end

거절

$ pnx 10-block-net.pnx
10-block-net.pnx:2:38: this block must leave the stack as it found it

(4a) 서명이 없는 값이 되는 블록은 아무것도 받지 않는다. 그 안에서 값이 필요한 낱말은 거절되고, 메시지가 (2a)의 두 꼴을 보인다.

10-word-value-bare.pnx

use str
word demo ()
  do str.starts_with end drop
end

거절

$ pnx 10-word-value-bare.pnx
10-word-value-bare.pnx:3:6: a block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end

(4b) forward 로 선언한 낱말은 정의된 뒤에만 값으로 준다.

10-word-value-forward.pnx

forward word later (n int)
word demo ()
  do later end drop
end
word later (n int) end

거절

$ pnx 10-word-value-forward.pnx
10-word-value-forward.pnx:3:6: a word declared with forward is given as a value only after its definition

10.6 줄기와 조합 낱말

(1) 줄기(stem)는 차례로 값을 내는 것이다. 줄기는 값이 아니어서 지역 변수에 넣지 못하고, 바로 뒤의 조합 낱말이 받아야 한다. 둘이 합쳐 루프 하나가 된다. 줄기를 내는 것은 다음과 같다.

줄기원소
배열, 목록의 이름칸의 값(레코드면 줄). 목록은 길이까지. 큐, 버퍼, 찾아보기는 줄기가 아니다
풀의 이름살아 있는 줄, 칸의 차례로. each 에만(8.9 (3a))
lo hi rangelo 부터 hi 앞까지의 int
s sep str.split, s str.lines, s str.words, s str.charsstr 뷰(16.1). chars 는 UTF-8 글자 하나씩
s str.bytesu8: 바이트(16.1)
s pat ptn.matches, s re rex.matches, s re rex.split맞은 부분(16.6, 16.7)
s csv.fields필드(16.9)

(2) 조합 낱말은 다음과 같다. “블록” 열은 블록이 받고 주는 것이다.

낱말블록결과
each(T)없음
each_index(T, i int)없음
fold(acc U, T gives U)U. 첫 값을 줄기 앞에 둔다: 0 xs do … end fold
count(T gives bool)int
any all(T gives bool)bool. 답이 정해지면 일찍 끝난다
first(T gives bool)T or fail. 없으면 err.not_found
sum없음int(dec(N) 의 줄기면 dec(N)). 넘치면 결함 overflow
minimum maximum없음int or fail(dec(N) 도 같다). 빈 줄기면 err.not_found
map_into(T gives U)뒤에 목적지. or fail
filter_into(T gives bool)뒤에 목적지. or fail
n times()없음. 줄기 없이 n 번

10-combinators.pnx

use io
use str
array scores int 6
assign scores (72, 95, 88, 61, 90, 79)
scores sum str.to_str io.println
scores do (s int gives bool) s 90 ge end count str.to_str io.println
scores do (s int gives bool) s 60 lt end any if "someone failed" else "all passed" end io.println
0 scores do (acc int, s int gives int) acc s max end fold str.to_str io.println
scores do (s int, i int) i 2 lt if cat ("#", i, " ", s) io.println end end each_index
1 5 range do (i int) i str.to_str io.print end each
"" io.println
3 do "x" io.print end times
"" io.println

출력

$ pnx 10-combinators.pnx
485
2
all passed
95
#0 72
#1 95
1234
xxx

10-first.pnx

use io
use str
array xs int 5
assign xs (3, 8, 12, 5, 20)
xs do (x int gives bool) x 10 gt end first or_else (e) -1 end str.to_str io.println
xs do (x int gives bool) x 100 gt end first or_else (e) -1 end str.to_str io.println
xs minimum or_else (e) 0 end str.to_str io.println

출력

$ pnx 10-first.pnx
12
-1
3

(3) map_into 와 filter_into 는 새 저장소를 만들지 않고 목적지에 쓴다. 목적지가 목록이면 끝에 push 하고 아무것도 주지 않는다. 목적지가 배열이면 0 번 칸부터 쓰고 쓴 칸의 수를 준다(gives n int or
fail
). 목적지가 차면 err.full 로 실패하고, 그때까지 쓴 것은 남는다. 목적지는 모음 매개변수여도 된다(9.9).

10-map-into.pnx

use io
use str
array xs int 4
assign xs (1, 2, 3, 4)
list squares int 4
array evens int 4
word go (or fail)
  xs do (x int gives int) x x mul end squares map_into or_fail
  xs do (x int gives bool) x 2 mod 0 eq end evens filter_into or_fail to n
  cat ("squares ", 3 squares idx, ", evens written ", n) io.println
end
go or_else (e) e err_text io.println end

출력

$ pnx 10-map-into.pnx
squares 16, evens written 2

(4) 줄기와 조합 낱말은 루프 하나를 이루고, each, each_index, times 의 블록 안에서 break 는 그 루프를 떠난다. 줄기는 멈추고 조합 낱말 뒤의 코드가 돈다. while 과 until 은 루프를 떠나듯 떠난다(10.2 (1)). 루프를 떠나는 규칙이 그대로다: 스택은 블록이 시작할 때와 같고(10.2 (2)), 블록의 미룬 코드가 돈다(10.7). 블록 안의 loop 에 쓴 break 는 그 루프를 떠난다. 모든 원소로 결과를 만드는 다른 조합 낱말의 블록은 일찍 떠나지 못하고, 블록 밖의 이름 붙은 루프는 블록 안에서 떠나지 못한다.

10-leave-each.pnx

use io
use str
word until_blank (s str)
  s str.lines do (line str)
    line str.len 0 eq if break end
    cat ("> ", line) io.println
  end each
  "done" io.println
end
"first\nsecond\n\nnot read" until_blank

출력

$ pnx 10-leave-each.pnx
> first
> second
done

10.7 미룬 코드

구문
defer = "defer" { body } "end" ;

(1) defer 코드 end 는 둘러싼 블록을 떠날 때 돌 코드를 적는다. 블록은 낱말의 몸, 루프의 몸(한 바퀴마다), 조합 낱말의 do 블록(한 번 돌 때마다)이다. 그 블록을 떠나는 모든 길에서 돈다: 블록의 end, 루프의 한 바퀴 끝, break, 루프를 떠나는 while·until, return, or_return, or_fail, fail. 여럿이면 적은 차례의 거꾸로 돈다. 결함으로 끝날 때는 돌지 않는다(그때는 상태가 핸들을 닫는다, 19.6).

10-defer.pnx

use io
use str
buffer trace 256
word say (s str) s trace append or_else (e) end end
rem Every way out of a block runs its defers, the last written first.
word step (n int or fail)
  "open " say
  defer "close " say end
  defer "flush " say end
  n 0 lt if err.range fail end
  "work " say
end
1 step or_else (e) end
-1 step or_else (e) "(failed) " say end
trace contents str.trim io.println

출력

$ pnx 10-defer.pnx
open work flush close open flush close (failed)

(2) 미룬 코드가 소유 값을 소비하면(m release, f file.close) 그 값은 미룬 코드가 맡는다. 블록을 떠나는 모든 길에서 소비된 것으로 치므로 실패 경로마다 닫는 코드를 따로 쓸 일이 없다. 블록 안에서는 그 값을 빌려 쓸 수 있다.

10-defer-owned.pnx

use io
use str
buffer report 4096
rem A mark taken back at the end of every turn, also when append fails.
word build (or fail)
  1 50 range do (i int)
    mark to m
    defer m release end
    cat ("row ", i, ": ", i i mul, "\n") report append or_fail
  end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println

출력

$ pnx 10-defer-owned.pnx
report: 585 bytes
제약

(3) defer 는 낱말의 몸, 루프의 몸, do 블록에 바로 쓴다. if·case·or_else 의 가지 안이나 정의 밖에 쓰면 거절된다.

10-defer-branch.pnx

word f (c bool)
  c if defer end end
end

거절

$ pnx 10-defer-branch.pnx
10-defer-branch.pnx:2:8: defer is written right in a word's body, a loop's body or a do block, not in a branch

(4) 미룬 코드는 스택을 받은 그대로 두고 아무것도 남기지 않는다. return fail break while until to set loop do case defer 와 or_fail or_return 을 쓸 수 없다. 실패하는 낱말은 그 안에서 or_else 로 다룬다.

(5) 미룬 코드가 맡은 소유 값을 블록 안에서 소비하면 거절된다.

10-defer-taken.pnx

word twice (gives int)
  mark to m
  defer m release end
  m release
  0
end

거절

$ pnx 10-defer-taken.pnx
10-defer-taken.pnx:4:5: a defer gives this owned value when the block is left; here it can only be lent
진단
조건메시지
두 갈래의 스택이 다름the two branches of this if leave different stacks
한 바퀴가 스택을 바꿈one turn of a loop must leave the stack as it found it
루프 밖의 while until breakwhile, until and break belong inside a loop
결과를 주는 낱말의 블록 안의 while until breakonly the block of each, each_index or times is left early; this word gives a result made from every element
do 블록 밖 루프로의 break (NAME)a named loop outside this block is not left from inside it; break leaves the block
닿지 않는 코드this code cannot be reached: the code before it always leaves
case 의 같은 값this value is already in an earlier branch of the case
펼쳐지는 블록이 스택을 바꿈this block must leave the stack as it found it
정의 밖의 returnreturn belongs inside a definition
상수가 아닌 가지의 값a case value is a literal or the name of a const
줄기가 아닌 것에 조합 낱말only an array, a list or a pool is a stem (not a queue, a buffer or an index)
가지 안의 deferdefer is written right in a word's body, a loop's body or a do block, not in a branch
정의 밖의 deferdefer belongs inside a definition
미룬 코드가 스택을 바꿈a defer leaves the stack as it found it
미룬 코드 안의 떠나는 낱말a defer holds no return, fail, break, while, until, to, set, loop, do, case or defer
미룬 코드 안의 or_fail or_returna defer does not leave the word; a failure in it is taken with or_else
미룬 코드가 맡은 값을 소비a defer gives this owned value when the block is left; here it can only be lent
서명이 없는 값이 되는 블록에서 값이 모자람a block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end
정의 전의 forward 낱말을 값으로a word declared with forward is given as a value only after its definition

10.8 문장의 끝

(1) 코드에 쓴 쉼표나 점(서명, cat, 이름 목록의 괄호 안이 아닌 곳)은 문장(statement)을 끝낸다: 마지막 쉼표나 점 뒤의 코드다. 둘 다 스택을 바꾸지 않고, 둘 다 꼭 써야 하는 것은 아니다. 둘이 없는 코드는 전과 같은 뜻이다.

(2) 쉼표는 아무것도 기다리지 않는다: 다음 문장이 시작할 때 이 문장의 효과가 아직 진행 중일 수 있다.

(3) 점은 장벽(barrier)이다: 점 앞에서 시작한 모든 효과가 점 뒤의 무엇보다 먼저 끝난다. 처리기의 몫은 호스트가 pinax_on_barrier(19.4 (3))로 준 함수를 부르는 것이고, 호스트의 몫은 호스트 낱말이 붙들고 있는 것(표준 출력, 파일에 쓴 것)을 내보내고 뒤에서 시작한 일을 기다리는 것이다. 적재한 코드의 끝과 세션 줄의 끝은 점이다. 결함으로 끝나도 그렇다. 데이터로 적재한 소스에서 둘은 문장을 나눌 뿐 아무것도 부르지 않는다.

(4) 문장이 결과를 써야 하는 효과(읽기, 명령의 출력)는 닿은 자리에서 끝낸다. 두 점 사이에서 같은 자원에 대한 효과는 프로그램의 순서를 지키고, 다른 자원에 대한 효과는 겹칠 수 있다. 이 판은 모든 효과를 프로그램의 순서대로 돌린다. 겹침은 뒤의 판이 쓸 수 있는 여지다.

10-statements.pnx

use io
word show (n int)
  cat ("square: ", n n mul) io.println .
end
"start" io.println ,
3 show ,
4 show .

출력

$ pnx 10-statements.pnx
start
square: 9
square: 16