15 도구
먼저 알아야 할 것: 1장(pnx), 3장(낱말).
이 장이 답하는 물음
- 낱말을 시험하려면 무엇을 쓰는가?
- 한 줄씩 해 보며 배우려면?
- 스택이 자리마다 어떤지 보거나, 들여쓰기를 고르게 하려면?
15.1 시험
test 이름 … end 는 시험을 정의한다. 시험은 받는 것도 주는 것도 없는 낱말이고, 적재할 때는 돌지 않는다. pnx test 파일 이 시험을 차례로 돌린다. assert 가 거짓이면 그 시험은 실패다.
15-test.pnx
use str
word initials (name str gives str)
name str.trim " " str.cut to (given, family)
cat (given 1 str.take, family str.trim 1 str.take) str.upper
end
test two_names "grace hopper" initials "GH" eq assert end
test spaces_around " ada lovelace " initials "AL" eq assert end
test one_name "plato" initials "P" eq assert end
시험 결과
$ pnx test 15-test.pnx
ok two_names
ok spaces_around
ok one_name
3 tests, 0 failed
15-test-fail.pnx
word clamp_percent (n int gives int) n 0 100 clamp end
test inside 50 clamp_percent 50 eq assert end
test above 150 clamp_percent 100 eq assert end
test wrong -5 clamp_percent -5 eq assert end
test-fail
$ pnx test 15-test-fail.pnx
ok inside
ok above
FAIL wrong (failed)
3 tests, 1 failed
clamp 는 음수를 0 으로 올리므로 wrong 시험은 틀린 기대를 적은 것이다. 실패한 시험이 있으면 pnx 는 1 로 끝난다.
흔한 오해: 비교만 적으면 시험이 된다
21 double 42 eq 는 bool 하나를 남길 뿐 아무것도 검사하지 않는다. 시험은 받는 것도 주는 것도 없는 낱말이라, 값을 남긴 시험은 거절된다. 끝에 assert 를 붙인다.15-test-leaves.pnx
word double (n int gives int) n 2 mul end
test doubles 21 double 42 eq end
거절
$ pnx 15-test-leaves.pnx
15-test-leaves.pnx:2:30: the stack does not hold exactly the results of the signature here
15.2 대화
pnx repl 은 한 줄씩 읽어 돌리고, 줄마다 스택을 보인다(맨 아래가 왼쪽). 스택은 Forth 처럼 줄에서 줄로 남는다: 뒤 줄은 앞 줄의 정의와 앞 줄이 남긴 값을 쓴다. 거절되거나 결함으로 끝난 줄은 스택을 그대로 둔다. clear 로 시작하는 줄은 빈 스택에서 시작한다.
15-repl.pnx
use str
2 3
add
word cube (n int gives int) n n mul n mul end
cube
"pinax" str.upper
1 0 div
cube
대화
$ pnx repl
2 3
5
5
125
125 "PINAX"
error: line 1: div_zero
1 0 div 줄은 결함으로 끝나 스택을 그대로 두었다. 마지막 줄 cube 는 스택 맨 위가 str 이라 거절되었다. 거절의 메시지는 표준 오류로 가므로 위의 출력에는 없다.
낱말이나 전역을 선언하는 줄은 그것이 더하는 만큼만 프로그램에 덧붙여지므로, 긴 대화에서도 줄 하나의 적재는 그 줄의 길이만큼 걸리고, 선언한 만큼 메모리 블록이 찬다. 값만 계산하는 줄은 블록을 쓰지 않는다. 블록의 4 분의 3 이 차면 pnx 가 표준 오류로 한 번 알린다. 큰 전역을 많이 선언하는 대화는 PNX_BLOCK_KB 로 블록을 키워 시작한다(기본 16384, 곧 16 MiB).
줄마다 스택에 무엇이 있는지 따라가면 이렇다. 다음 줄은 그 스택에서 시작한다.
| 토큰 | 그 뒤의 스택 | 일어난 일 |
|---|---|---|
2 3 | 2 3 | 값 둘이 스택에 남는다. |
add | 5 | 다음 줄이 그것을 가져간다. |
word cube … end | 5 | 정의는 스택을 그대로 둔다. |
cube | 125 | 앞 줄들의 정의와 값을 쓴다. |
"pinax" str.upper | 125 "PINAX" | str 도 255 바이트까지 남는다. |
1 0 div | 125 "PINAX" | 결함으로 끝난 줄은 스택을 그대로 둔다. |
cube | 125 "PINAX" | 거절된다: 맨 위가 int 가 아니라 str 이다. |
낱말은 언제든 다시 정의할 수 있다. 그 이름으로 word 를 한 번 더 쓰면 된다. 서명이 같으면 그 낱말을 부르는 모든 낱말이 새것을 부르므로, 고친 것이 모든 호출자에 닿는다. 서명이 다르면 새 낱말이 이름을 가져가고, 그 전에 컴파일된 낱말들은 Forth 처럼 옛 몸을 계속 부른다. pnx repl 이 어느 낱말들인지 알린다.
15-redefine.pnx
word sq (n int gives int) n n mul end
word quad (n int gives int) n sq sq end
3 quad
word sq (n int gives int) n n add end
clear 3 quad
word sq (a int, b int gives int) a b mul end
clear 3 quad
4 sq
대화
$ pnx repl
81
81
12
1 word still calls the old sq: quad
12
12
48
첫 번째 재정의 뒤에 quad 는 두 번 두 배를 한다(12). 두 번째 뒤에 sq 는 두 값을 곱하고, quad 는 여전히 옛 sq 를 돈다. 파일의 낱말은 파일에서 고치고 다시 적재한다.
문장은 쉼표나 점으로 끝낼 수 있다. 쉼표는 아무것도 기다리지 않는다. 점은 장벽이다: 앞의 문장들이 시작한 일이 거기서 끝나므로, 버퍼에 붙들린 출력도 그때 나간다. 둘 다 꼭 써야 하는 것은 아니다. 프롬프트에서 줄의 끝과 프로그램 코드의 끝은 이미 점이다.
15-dot.pnx
use io
word countdown (n int)
0 n range do (i int) cat (n i sub, " ") io.print , end each
"go" io.println .
end
3 countdown
출력
$ pnx 15-dot.pnx
3 2 1 go
bash 로 치면 쉼표는 &, 점은 wait 와 비슷하다. 앞의 것이 보이거나 일어난 뒤에 넘어가야 하는 자리에 점을 쓴다: 오래 걸리는 계산 앞, 진행을 알리는 줄 뒤.
pnx see --effects 는 아무것도 돌리지 않고, 파일의 낱말마다 제 스택과 지역 변수 밖에서 할 수 있는 일을 알려 준다: 읽고 쓰는 전역, 닿는 호스트 모듈, 양보하거나 실패할 수 있는지. none 인 낱말은 같은 입력에 같은 결과를 주고 다른 것에 닿지 않는다.
15-effects.pnx
use io
use file
global hits int
word double (n int gives int) n 2 mul end
word tally (n int) hits n add set hits end
word save (path str or fail)
path file.open_write or_fail to f
defer f file.close end
cat ("hits ", hits) f file.write or_fail
end
word say () cat ("hits ", hits double) io.println end
낱말마다 효과
$ pnx see --effects 15-effects.pnx
double: none
tally: reads hits; writes hits
save: reads hits; file reads and writes; may fail
say: reads hits; io writes
15.3 셸 스크립트
모듈 sh 는 시스템 셸로 명령을 돌리고, 찾는 순서를 쓰면 그 낱말과 io 의 낱말을 맨 이름으로 쓸 수 있다. #!/usr/bin/env pnx 로 시작하는 파일은 명령처럼 돈다.
15-shell.pnx
search (io, sh)
word main (or fail)
"echo first" run or_fail drop
cat ("the shell says: ", "echo hello" output or_fail) print
"exit 2" run or_fail to s
cat ("exit 2 gave status ", s) println
end
main or_else (e) cat ("failed: ", e err_text) println end
출력
$ pnx 15-shell.pnx
first
the shell says: hello
exit 2 gave status 2
명령의 상태는 실패가 아니라 값이다: "exit 2" run 은 2 를 준다. 셸을 띄우지 못할 때만 실패한다. 자료를 명령에 넣을 때는 먼저 따옴표로 싼다: cat ("ls ", name sh.quote) run. sh.open_read 와 sh.open_write 는 관을 file 로 주고, file 의 낱말로 읽고 쓴다. 닫으면 명령이 끝나기를 기다리고, 그때 sh.status 가 어떻게 끝났는지 알려 준다. sh.exit 는 프로그램을 제 상태로 끝낸다. Pinax 를 넣어 쓰는 호스트는 sh 를 등록할 때만 그것을 가진다(표준 17.5).
15.4 스택 보기와 정리
pnx see 는 정의의 줄마다 그 줄이 끝난 자리의 스택을 보인다. 스택이 머릿속과 다를 때 쓴다.
15-see.pnx
word mean (a int, b int, c int gives int)
a b add
c add
3 div
end
목록
$ pnx see 15-see.pnx
2 a b add | int
3 c add | int
4 3 div | int
줄 번호 오른쪽은 그 줄의 코드, | 오른쪽은 그 줄이 끝난 자리의 스택이다. 아래가 왼쪽, 맨 위가 오른쪽이다 — 이 책의 추적 표와 같다. 표준 모듈이나 cat 이 섞여 스택이 헷갈릴 때 특히 쓸모 있다.
거절되는 소스에도 pnx see 를 쓸 수 있다. 거절되는 자리까지의 줄마다 스택을 보이고, 끝에 거절 메시지를 낸다. 그래서 “스택에 무엇이 남았는가” 를 메시지보다 먼저 눈으로 볼 수 있다.
15-see-wrong.pnx
word price_label (cents int gives str)
cents 100 div
"won" swap
cat (cents, " cents")
end
목록과 거절
$ pnx see 15-see-wrong.pnx
2 cents 100 div | int
3 "won" swap | str int
4 cat (cents, " cents") | str int str
15-see-wrong.pnx:5:1: the stack does not hold exactly the results of the signature here
서명은 str 하나를 약속했는데, 마지막 줄의 스택은 str int str 이다. 3째 줄의 swap 이 "won" 을 아래로 보냈고, 2째 줄의 몫(int)은 아무도 쓰지 않았다. 둘째와 셋째 줄을 지우면 cat 의 결과 하나만 남는다.
pnx fmt 은 들여쓰기와 공백만 고친다. 토큰과 줄의 자리, 주석은 그대로 둔다.
15-fmt.pnx
use io
word fizz (n int gives str)
n 15 mod 0 eq if "FizzBuzz" return end
n 3 mod 0 eq if "Fizz" return end
n 5 mod 0 eq if "Buzz" else cat (n) end
end
정리한 모양
$ pnx fmt 15-fmt.pnx
use io
word fizz (n int gives str)
n 15 mod 0 eq if "FizzBuzz" return end
n 3 mod 0 eq if "Fizz" return end
n 5 mod 0 eq if "Buzz" else cat (n) end
end
문답
물음. 시험은 따로 파일에 두는가, 낱말 옆에 두는가?
답. 어느 쪽이든 된다. pnx test 파일 은 그 파일을 적재한 뒤 그 안의 시험만 돌린다. 낱말 옆에 두면 낱말을 고칠 때 시험이 눈에 띈다. 시험이 많아지면 use 로 모듈을 불러오는 시험 파일을 따로 둔다.
문답
물음. pnx see 의 스택은 왜 타입만 보이고 값은 없는가?
답. see 는 돌리지 않고 적재만 한다. 적재 때 처리기가 아는 것은 스택의 깊이와 타입뿐이고, 값은 돌아야 생긴다. 값까지 보려면 pnx repl 에서 한 줄씩 돌린다.
정리
test … end와pnx test, 실패한 시험은 종료 코드 1(표준 20.2).pnx repl은 줄마다 남긴 값을 보인다(20.3).pnx see는 줄마다의 스택을 보인다. 거절되는 소스에서는 그 자리까지 보이고 메시지를 낸다(20.4).pnx fmt은 들여쓰기와 공백만 고친다(20.4).