3 Words and signatures
Builds on: Chapter 1 (making words), chapter 2 (the stack).
Questions this chapter answers
- What goes into a signature, and what does the processor check with it?
- What is the difference between naming an input and not?
- How are words with two results, and words that call themselves, written?
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.
| Token | Stack after it | What happened |
|---|---|---|
365 | 365 | The value goes on. |
c_to_f | 977 | As 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.
| Token | Stack after it | What happened |
|---|---|---|
| (the body starts) | — | a is 4 and b is 8. The stack is empty. |
a b | 4 8 | Using a name puts its value on. |
add | 12 | Adds. |
2 div | 6 | Divides 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.
| Token | Stack after it | What happened |
|---|---|---|
| (the body starts) | 7 | A bare input stays on the stack. |
dup | 7 7 | One more. |
mul | 49 | Multiplies. 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 assum, 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
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
- Every word has a signature, checked where it is called and in its body (standard 9.2).
- The value put on first is the first input. The body starts with an empty stack (bare inputs stay on it) and ends with only its results (9.2).
- Named inputs become locals; bare inputs stay on the stack; they do not mix (9.2).
- Several results are taken with
to (a, b); the rightmost is the top (9.4). - A visible name (core words included) cannot be used for a new name (9.5).
- Tail calls do not grow the depth (9.6).