15 Tools

Builds on: Chapter 1 (pnx), chapter 3 (words).

Questions this chapter answers

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.

TokenStack after itWhat happened
2 32 3Two values stay on the stack.
add5The next line takes them.
word cube … end5A definition leaves the stack as it is.
cube125Uses the definition and the value of the lines before.
"pinax" str.upper125 "PINAX"A str stays too, up to 255 bytes.
1 0 div125 "PINAX"A line that ends with a fault leaves the stack as it was.
cube125 "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