3 Words and signatures

Builds on: Chapter 1 (making words), chapter 2 (the stack).

Questions this chapter answers

3.1 What a signature does

Every word has a signature: its inputs, gives, its results, and or fail if it can fail. The processor checks the signature from both sides: where the word is called, that the top values of the stack match the input types; in the body, that the stack is exactly the results when the body ends.

03-temperature.pnx

use io
use str
rem Celsius and Fahrenheit, in tenths of a degree so that everything stays an int.
word c_to_f (tenths int gives int) tenths 9 mul 5 div 320 add end
word f_to_c (tenths int gives int) tenths 320 sub 5 mul 9 div end
word show (tenths int gives str)                  rem 365 -> "36.5"
  tenths 10 div to whole
  tenths 10 mod abs to part
  cat (whole, ".", part)
end
cat ("36.5 C = ", 365 c_to_f show, " F") io.println
cat ("98.6 F = ", 986 f_to_c show, " C") io.println

Output

$ pnx 03-temperature.pnx
36.5 C = 97.7 F
98.6 F = 37.0 C

show takes one int and gives one str, so 365 c_to_f show can be one item of a cat. Temperatures are kept in tenths of a degree as ints because this edition has no floating point.

A signature is one line that says what the word does to the stack. (tenths int gives int) means “takes one int off the top and puts one int on”. So the caller’s stack can be followed without reading the body.

TokenStack after itWhat happened
365365The value goes on.
c_to_f977As its signature says: one int off, one int on.
show"97.7"One int off, one str on.

3.2 What happens at a call

At a call the processor takes as many values off the top of the stack as the signature has inputs, and gives them to the word. The value put on first is the first input. The body of the word starts with an empty stack — the caller’s values further down can be neither seen nor touched. When the body ends the stack shall hold exactly the results of the signature, and those values go onto the caller’s stack.

03-arg-order.pnx

use io
use str
word minus (a int, b int gives int) a b sub end   rem a - b
10 3 minus str.to_str io.println                  rem 10 was pushed first, so a = 10 and b = 3: 7
3 10 minus str.to_str io.println                  rem the other way round: -7
word half_of_sum (a int, b int gives int)
  a b add                                         rem the body starts with an empty stack
  2 div                                           rem and must end holding one int
end
4 8 half_of_sum str.to_str io.println             rem (4 + 8) / 2 = 6

Output

$ pnx 03-arg-order.pnx
7
-7
6

In 10 3 minus, 10 went on first, so a is 10 and b is 3 — the same as the “first on the left” rule of subtraction (chapter 2). Follow the body of half_of_sum. The caller’s stack held 4 8; inside the body they are the names a and b, and the stack starts empty.

TokenStack after itWhat happened
(the body starts)—a is 4 and b is 8. The stack is empty.
a b4 8Using a name puts its value on.
add12Adds.
2 div6Divides by 2. One int when the body ends — as the signature says.

3.3 Named inputs and bare inputs

An input with a name (a int) becomes a local of that name, and the body starts with an empty stack. An input given as a type only (int) stays on the stack when the body starts. One signature cannot mix the two.

03-two-results.pnx

use io
word divmod (a int, b int gives q int, r int) a b div a b mod end   rem two results
word bare_square (int gives int) dup mul end                          rem a bare input stays on the stack
word show () 17 5 divmod to (q, r) cat ("17 = 5 * ", q, " + ", r) io.println end
show
cat ("7 squared is ", 7 bare_square) io.println

Output

$ pnx 03-two-results.pnx
17 = 5 * 3 + 2
7 squared is 49

divmod has two results. The names q and r on the results side are for the reader only; they make no locals. The caller takes both at once with to (q, r). The rightmost name takes the top value — in the order of the signature.

bare_square has no name for its input. Then the value does not become a name: the body starts with it still on the stack. The body of 7 bare_square starts with one 7 on the stack, and dup mul squares it.

TokenStack after itWhat happened
(the body starts)7A bare input stays on the stack.
dup7 7One more.
mul49Multiplies. One result is left.

One signature cannot mix named and bare inputs: it would be unclear which values stay on the stack.

03-mixed-inputs.pnx

word scale (n int, int gives int) n mul end       rem one input named, the other bare

Refused

$ pnx 03-mixed-inputs.pnx
03-mixed-inputs.pnx:1:33: inputs are all named or all bare types, not a mix

Q&A

Q. With three or more inputs, which is better?

A. Name them. Two values can be handled with dup and swap, but from three on the stack is hard to follow in one’s head. That is also why Pinax has no rot or pick.

3.4 Programs that break their signature

03-result-wrong.pnx

word area (w int, h int gives int) w h mul w end

Refused

$ pnx 03-result-wrong.pnx
03-result-wrong.pnx:1:46: the stack does not hold exactly the results of the signature here

When the body ends there are two values on the stack (the result of w h mul, and w), but the signature promised one.

03-call-wrong.pnx

use io
word greet (name str) cat ("Hi ", name) io.println end
42 greet

Refused

$ pnx 03-call-wrong.pnx
03-call-wrong.pnx:3:4: an argument has another type than the word's signature asks for

greet takes a str, and was given 42. The check happens before running, so such a mistake always shows, whether that line would ever run or not.

A word without results (a signature without gives) shall end its body with an empty stack. To give a computed value back, write the result in the signature: (n int gives int).

03-no-result.pnx

use io
word double (n int) n 2 mul end                   rem the signature gives nothing, yet a value is left

Refused

$ pnx 03-no-result.pnx
03-no-result.pnx:2:29: the stack does not hold exactly the results of the signature here

Common mistake: Naming a variable after a core word

Common words such as sum, count, first and add are all names of core words. Pinax does not let a visible name be hidden by another (no hiding), so parameters and locals with those names are refused.

03-taken-name.pnx

word average (sum int, count int gives int) sum count div end

Refused

$ pnx 03-taken-name.pnx
03-taken-name.pnx:1:45: sum is a word of the language; choose another name
Use other names such as total, n or items. Reserved words such as of, end and text cannot be names either.

3.5 Words that call themselves

03-recursion.pnx

use io
word gcd (a int, b int gives int) b 0 eq if a else b a b mod gcd end end   rem a tail call
word fib (n int gives int) n 2 lt if n else n 1 sub fib n 2 sub fib add end end
cat ("gcd(1071, 462) = ", 1071 462 gcd) io.println
cat ("fib(20) = ", 20 fib) io.println

Output

$ pnx 03-recursion.pnx
gcd(1071, 462) = 21
fib(20) = 6765

A word may call itself without announcing it. A tail call — a call that is the last thing a word does, as in gcd — reuses the place, so the depth does not grow. A call with work left after it, as in fib, piles up depth, and too deep stops with the fault depth. Two words that call each other announce one with forward word.

Q&A

Q. A word’s body is long and I lose the stack. What do I do?

A. pnx see FILE shows the stack at the end of every line of a definition (chapter 15). Write the body one step per line, and name the values in the middle with to NAME (chapter 4): then no more than two values are on the stack at once.

Summary