15 Tools
Builds on: Chapter 1 (pnx), chapter 3 (words).
Questions this chapter answers
- What do I use to test words?
- How do I try things out a line at a time?
- How do I see the stack at every line, or make indentation even?
15.1 Tests
test NAME … end defines a test: a word that takes and gives nothing, and does not run at load. pnx test FILE runs the tests in order. A false assert makes that test fail.
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
Test run
$ 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 raises a negative to 0, so the test wrong writes down a wrong expectation. When a test fails, pnx exits with 1.
Common mistake: Writing just the comparison makes a test
21 double 42 eq only leaves a bool; it checks nothing. A test takes and gives nothing, so a test that leaves a value is refused. End it with assert.15-test-leaves.pnx
word double (n int gives int) n 2 mul end
test doubles 21 double 42 eq end
Refused
$ 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 Sessions
pnx repl reads and runs one line at a time and shows the stack after each line, the bottom on the left. The stack stays from line to line, as in Forth: later lines use the definitions of earlier ones and the values they left. A line that is refused or ends with a fault leaves the stack as it was. A line that begins with clear starts from an empty stack.
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
Session
$ pnx repl
2 3
5
5
125
125 "PINAX"
error: line 1: div_zero
The line 1 0 div ended with a fault and left the stack as it was. The last line, cube, was refused because the top of the stack is a str. The message of a refusal goes to standard error, so it is not in the output above.
A line that declares a word or a global adds just what it declares to the program, so even in a long session a line loads in the time its own length takes, and the memory block fills with what was declared. A line that only computes values uses none of it. When three quarters of the block are used, pnx says so once on standard error. Start a session that declares many large globals with a larger block through PNX_BLOCK_KB (the default is 16384, that is 16 MiB).
What the stack holds after each line, followed. The next line starts from it.
| Token | Stack after it | What happened |
|---|---|---|
2 3 | 2 3 | Two values stay on the stack. |
add | 5 | The next line takes them. |
word cube … end | 5 | A definition leaves the stack as it is. |
cube | 125 | Uses the definition and the value of the lines before. |
"pinax" str.upper | 125 "PINAX" | A str stays too, up to 255 bytes. |
1 0 div | 125 "PINAX" | A line that ends with a fault leaves the stack as it was. |
cube | 125 "PINAX" | Refused: the top is a str, not an int. |
A word can be defined again at any time: write word with its name once more. When the signature is the same, every word that calls it calls the new one, so a fix reaches every caller. When the signature is another, the new word takes the name, and the words compiled before keep calling the old body, as in Forth; pnx repl says which they are.
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
Session
$ pnx repl
81
81
12
1 word still calls the old sq: quad
12
12
48
After the first redefinition quad doubles twice (12). After the second, sq multiplies two values, and quad still runs the old sq. A word of a file is changed in the file, then loaded again.
A statement may end with a comma or a dot. A comma waits for nothing. A dot is a barrier: what the statements before it started is finished there, so output held in a buffer is written. Neither is required; the end of a line at the prompt, and the end of a program’s code, are a dot already.
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
Output
$ pnx 15-dot.pnx
3 2 1 go
In bash terms, a comma is like & and a dot like wait. Use a dot where what came before must be seen, or must have happened, before going on: before a long computation, after a progress line.
pnx see --effects tells, without running anything, what each word of a file may do beyond its own stack and locals: the globals it reads and writes, the host modules it reaches, whether it may yield or fail. A word marked none gives the same results for the same inputs and touches nothing else.
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
The effects of each word
$ 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 Shell scripts
The module sh runs commands through the system shell, and a search order lets its words and those of io be written bare. A file that begins with #!/usr/bin/env pnx runs as a command.
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
Output
$ pnx 15-shell.pnx
first
the shell says: hello
exit 2 gave status 2
A command’s status is a value, not a failure: "exit 2" run gives 2. Only a shell that cannot start fails. To put data into a command, quote it first: cat ("ls ", name sh.quote) run. sh.open_read and sh.open_write give a pipe as a file, read and written with the words of file; closing it waits for the command, and sh.status then tells how it ended. sh.exit ends the program with a status of its own. A host that embeds Pinax has sh only when it registers it (standard 17.5).
15.4 Seeing the stack, and formatting
pnx see shows, for each line of a definition, the stack where that line ends. Use it when the stack is not what you think.
15-see.pnx
word mean (a int, b int, c int gives int)
a b add
c add
3 div
end
Listing
$ pnx see 15-see.pnx
2 a b add | int
3 c add | int
4 3 div | int
Right of the line number is the code of that line; right of | is the stack where the line ends, the bottom on the left and the top on the right — as in the trace tables of this book. It is most useful when standard modules or cat make the stack hard to follow.
pnx see also works on a source it refuses. It shows the stack after each line up to the place of the refusal, then gives the message. So what is left on the stack can be seen before the message is read.
15-see-wrong.pnx
word price_label (cents int gives str)
cents 100 div
"won" swap
cat (cents, " cents")
end
Listing, then the refusal
$ 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
The signature promised one str, but the stack after the last line is str int str. The swap of line 3 sent "won" down, and the result of line 2 (an int) was used by nothing. Removing lines 2 and 3 leaves only the result of cat.
pnx fmt changes only indentation and spaces. Tokens, the places of lines and remarks stay as they are.
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
Formatted
$ 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
Q&A
Q. Do tests go in a file of their own, or beside the words?
A. Either. pnx test FILE loads the file and then runs only the tests in it. Beside the words, a test is in sight when the word is changed. When there are many, a test file of its own brings the module in with use.
Q&A
Q. Why does the stack of pnx see show types only, and no values?
A. see loads and does not run. At load the processor knows the depth and types of the stack, nothing more; values exist only when the program runs. To see values, run a line at a time in pnx repl.
Summary
test … endandpnx test; a failed test makes the exit status 1 (standard 20.2).pnx replshows what each line leaves (20.3).pnx seeshows the stack after each line; on a refused source, up to the place, then the message (20.4).pnx fmtchanges only indentation and spaces (20.4).