2 Values and the stack
Builds on: Chapter 1 (the order of code, pnx, remarks).
Questions this chapter answers
- What is the stack, and how do values and words move on it?
- How is an ordinary formula put into the order Pinax reads?
- When does the processor check the stack, and what does it refuse?
- What values does Pinax have, and what happens when an integer overflows?
This chapter is about the stack, the ground Pinax stands on. It is the least familiar part for someone who has used other languages, but it has only a few rules. Follow the examples of this chapter through the tables, one row at a time, and the rest of the book reads much more easily.
2.1 What the stack is
The stack is a place where values are piled up, like a pile of plates. A new plate goes on top, and a plate is taken from the top. Nobody pulls a plate out of the middle.
Pinax code runs on this stack. The processor reads the code left to right, top to bottom, one token at a time, and does one of two things with each token.
- A value (
2,"abc",true) goes on top of the stack. - A word (
add,io.println) is set to work. The word takes as many values as it needs off the top, does its work, and puts its results, if any, on top.
Follow 2 3 add by these rules. This book uses tables like the one below often. The top of the stack is written on the right, and — is an empty stack.
| Token | Stack after it | What happened |
|---|---|---|
2 | 2 | A value: it goes on. |
3 | 2 3 | A value: it goes on top of 2. The top is now 3. |
add | 5 | A word: it takes the top two values 2 and 3 off, adds them, and puts the result 5 on. |
What other languages write 2 + 3 is 2 3 add in Pinax. The values come first and the work after: this is the postfix notation of chapter 1. It looks backwards at first; read it as “lay out what is needed, then set the work going” and it becomes natural.
2.2 Computing on the stack
02-stack.pnx
use io
use str
2 3 add 4 mul str.to_str io.println rem (2 + 3) * 4
2 3 4 mul add str.to_str io.println rem 2 + 3 * 4
10 3 sub str.to_str io.println rem the first value is on the left: 10 - 3
10 3 swap sub str.to_str io.println rem swap turns it round: 3 - 10
Output
$ pnx 02-stack.pnx
20
14
7
-7
Follow the first line, 2 3 add 4 mul, through a table.
| Token | Stack after it | What happened |
|---|---|---|
2 | 2 | Goes on. |
3 | 2 3 | Goes on. |
add | 5 | Adds the two values. |
4 | 5 4 | Goes on. |
mul | 20 | Multiplies the two values. |
str.to_str | "20" | Turns the integer into text: one value off, one on. |
io.println | — | Prints the text. The stack is empty. |
The second line, 2 3 4 mul add, piles three values up first. mul multiplies the top two, 3 and 4, and add adds the 2 left below to that result. So it is .
| Token | Stack after it | What happened |
|---|---|---|
2 3 4 | 2 3 4 | Three values go on in turn. |
mul | 2 12 | Multiplies the top 3 and 4. 2 stays below. |
add | 14 | Adds 2 and 12. |
Without parentheses, the order of the computation is the order of the text. Where a word is written decides what is computed first.
2.3 Turning a formula into postfix
A familiar formula is carried over in this order.
- With parentheses, carry the inside over first.
- Write one operation as “left value, right value, operation word”: is
a b sub. - Carry multiplication and division over before addition and subtraction, as in arithmetic.
- Treat a carried-over piece as one value again, and put it where the left or right value of the outer operation goes.
For the left bracket is 8 3 sub, the right one 2 4 add, and mul after the two pieces makes 8 3 sub 2 4 add mul.
02-infix.pnx
use io
use str
rem (8 - 3) * (2 + 4): each bracket first, then the operation after its two values
8 3 sub 2 4 add mul str.to_str io.println rem 5 * 6 = 30
rem 100 - 4 * 5 * 2: the multiplications first, as in arithmetic
100 4 5 mul 2 mul sub str.to_str io.println rem 100 - 40 = 60
rem (1 + 2 + 3 + 4) / 2: add as you go
1 2 add 3 add 4 add 2 div str.to_str io.println rem 10 / 2 = 5
rem 12 * 12 + 1: dup copies 12 instead of writing it twice
12 dup mul 1 add str.to_str io.println rem 145
Output
$ pnx 02-infix.pnx
30
60
5
145
| Token | Stack after it | What happened |
|---|---|---|
8 3 | 8 3 | The two values of the left bracket. |
sub | 5 | . |
2 4 | 5 2 4 | The two values of the right bracket. 5 waits below. |
add | 5 6 | . |
mul | 30 | Multiplies the results of the two brackets. |
Q&A
Q. I lose track of which value goes where. Is there an easier way?
A. Compute one piece per line and give its result a name (the locals of chapter 4). With 8 3 sub to left you do not have to keep the stack in your head for long. Pinax keeps the stack short and asks for names when it grows.
2.4 Words where order matters
Addition and multiplication give the same result whichever way round the two values are. Subtraction, division, remainder and comparison do not. There is one rule: the value put on first is on the left. 10 3 sub is , and 3 10 lt asks “is ?”.
02-order.pnx
use io
use str
10 3 sub str.to_str io.println rem 10 - 3 = 7: the value pushed first is on the left
10 3 div str.to_str io.println rem 10 / 3 = 3 (whole numbers: the rest is cut off)
10 3 mod str.to_str io.println rem the rest of 10 / 3 is 1
3 10 lt if "3 is less than 10" io.println end rem lt asks: first < second?
Output
$ pnx 02-order.pnx
7
3
1
3 is less than 10
div divides whole numbers and throws the remainder away; mod gives the remainder.
2.5 Five words that rearrange the stack
Five words change only the shape of the stack and compute nothing. In the table a and b are any values, and the right is the top.
| Word | Before | After | What it does |
|---|---|---|---|
dup | a | a a | Makes one more of the top value. |
drop | a | — | Throws the top value away. |
swap | a b | b a | Exchanges the top two values. |
over | a b | a b a | Puts one more of the second value on top. |
nip | a b | b | Throws the second value away. |
02-stack-words.pnx
use io
use str
rem dup: a becomes a a
7 dup mul str.to_str io.println rem 7 * 7 = 49
rem drop: a b becomes a
1 2 drop str.to_str io.println rem 2 is thrown away; 1 is printed
rem swap: a b becomes b a
1 2 swap sub str.to_str io.println rem 2 - 1 = 1
rem over: a b becomes a b a
5 8 over sub str.to_str io.println rem 8 - 5 = 3 is printed; 5 is still there
drop rem so it is thrown away here
rem nip: a b becomes b
5 8 nip str.to_str io.println rem 8
Output
$ pnx 02-stack-words.pnx
49
1
1
3
8
Follow the over line. over copies the value below, so 5 stays where it was. It has to be dropped after the printing — otherwise a value is left at the end of the program and the program is refused.
| Token | Stack after it | What happened |
|---|---|---|
5 8 | 5 8 | Two values go on. |
over | 5 8 5 | Copies the second value 5 to the top. |
sub | 5 3 | ; only the top two are used. |
str.to_str io.println | 5 | Prints 3. 5 is left. |
drop | — | Throws the remaining 5 away. |
Common mistake: There is a rot that brings the third value to the top
rot or pick, are left out on purpose: once more than three values are in play the stack is hard to follow in your head. Give the values names instead (named inputs in chapter 3, locals in chapter 4).02-rot.pnx
use io
use str
1 2 3 rot add add str.to_str io.println rem there is no rot: name the values instead
Refused
$ pnx 02-rot.pnx
02-rot.pnx:3:7: this name is not declared before this place
2.6 Types of values
02-values.pnx
use io
use str
use fmt
42 str.to_str io.println
-7 str.to_str io.println
0xff str.to_str io.println rem 255
1_000_000 str.to_str io.println
'A' str.to_str io.println rem a character is a u8, its byte
3 4 lt if "3 < 4 is true" else "false" end io.println
"a string" str.len str.to_str io.println
255 fmt.hex io.println
Output
$ pnx 02-values.pnx
42
-7
255
1000000
65
3 < 4 is true
8
ff
This book mostly uses four types:
int— 64-bit integers, written in decimal, hexadecimal (0xff) or binary (0b101), with underscores between digits if you like. A character'A'is theu8of its byte.u8u16u32u64i8i16i32are the integer types of those widths:255 u8makes one from anint,rawgives theintback, and none of them mixes withintin arithmetic.bool—trueandfalse; comparison words (lteq…) give them andiftakes them.str— strings; chapter 5 covers them.err— the reason for a failure; see chapter 8.
Q&A
Q. Do int and bool convert into each other?
A. No. 1 is not true, and you cannot add to true. Only a bool goes where a condition goes, and only an int into arithmetic.
2.7 The processor knows the stack in advance
Before the program runs — at load — the processor already knows, at every place, how many values are on the stack and the type of each. It draws the tables above for itself, in advance. So a mistake with the stack always shows before the program runs, whether or not that line would ever run. There are three such mistakes.
First, too few values. add needs two values and the stack holds one.
02-stack-short.pnx
use io
use str
5 add str.to_str io.println
Refused
$ pnx 02-stack-short.pnx
02-stack-short.pnx:3:3: the stack does not hold enough values for this word
Second, the wrong type. A string and an integer cannot be added.
02-types-mixed.pnx
use io
"3" 4 add drop
Refused
$ pnx 02-types-mixed.pnx
02-types-mixed.pnx:2:7: this word takes two int values, or two of the same declared type
Third, values left over. A value that nothing took when the program ends (and when the body of a word ends, chapter 3) is refused: a left-over value is usually the trace of forgotten work.
02-leftover.pnx
use io
use str
1 2 add str.to_str io.println
1 2 rem two values that nothing takes
Refused
$ pnx 02-leftover.pnx
02-leftover.pnx:5:1: values are left on the stack at the end of the source
5:1 in the message is the first column of the fifth line: where the source ends. Left-over values can only be known at the end, so the end is pointed at.
Common mistake: The depth of the stack may change as a program runs
Languages like Forth allow words that leave a different number of values depending on what happens. Pinax does not. At every place the depth and types of the stack are one and fixed. So the two branches of anif leave the same stack, and one turn of a loop leaves the stack as it was (chapter 4). This rule is what makes the checks above possible.2.8 Overflow is a fault
02-overflow.pnx
use io
use str
word double (n int gives int) n 2 mul end
4611686018427387903 double str.to_str io.println
4611686018427387904 double str.to_str io.println
Fault
$ pnx 02-overflow.pnx
9223372036854775806
02-overflow.pnx:3: integer overflow
n int = 4611686018427387904
A result beyond the range of int does not quietly wrap around. The run stops on the spot and the processor reports a fault. pnx shows the line of the fault and the locals of the word. For arithmetic that wraps use wrap_mul, for arithmetic that stops at the edge sat_mul, and to receive overflow as a failure chk_mul.
A fault is not the same as a refusal at load. Whether a sum overflows depends on the values and cannot be known before the run, so the run stops where it happens. Either way, memory is never damaged, and no wrong value goes quietly on.
2.9 Decimals
For money and other numbers with a fixed number of digits after the point, write them with the point: 4.20 is a dec(2), a decimal with two digits after the point. It is held as the integer 420, so sums come out exactly, and the same on every machine. Add and subtract it with the same dec(N), multiply and divide it by an int; two dec multiplied keep all their digits. rescale sets the number of digits, rounding half away from zero.
02-dec.pnx
use io
word demo ()
4.20 to coffee rem dec(2): two digits after the point
coffee 3 mul 1.15 add to bill rem times an int, plus another dec(2)
bill 0.10 mul to tip rem dec(2) times dec(2): dec(4), nothing lost
cat ("bill ", bill, ", tip ", tip, ", rounded ", tip 2 rescale) io.println
cat ("split in three: ", bill 3 div) io.println
end
demo
Output
$ pnx 02-dec.pnx
bill 13.75, tip 1.3750, rounded 1.38
split in three: 4.58
Dividing by an int cuts toward zero, as for integers: 9.75 3 div is 3.25, but 10.00 3 div is 3.33, and one cent is left over. When that matters, take the cents with raw and share them out yourself.
Common mistake: 4.20 and 0.5 can be added
dec(2) and dec(1), two types. Write 0.50, or rescale one.02-dec-scales.pnx
word demo () 4.20 0.5 add drop end rem dec(2) and dec(1)
Refused
$ pnx 02-dec-scales.pnx
02-dec-scales.pnx:1:23: two dec values of different scales: rescale one first (d 2 rescale)
Summary
- Values go on the stack; a word takes values off the top, works, and puts its results on (standard 9.3).
- The value put on first is on the left:
10 3 subis 7 (standard 9.3). - The stack words are
dupdropswapoverniponly; when values pile up, name them (9.3). - The processor checks the depth and types of the stack at load: too few values, the wrong type and values left over are refused (7.1, 9.2, 9.3).
- A number with a point is a
dec(N): N digits after it, exact arithmetic,rescaleto round (standard 7.5). - Integer overflow and division by zero are faults; other arithmetic words exist (7.1, 15.1).