2 Values and the stack

Builds on: Chapter 1 (the order of code, pnx, remarks).

Questions this chapter answers

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.

  1. A value (2, "abc", true) goes on top of the stack.
  2. 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.

TokenStack after itWhat happened
22A value: it goes on.
32 3A value: it goes on top of 2. The top is now 3.
add5A 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.

TokenStack after itWhat happened
22Goes on.
32 3Goes on.
add5Adds the two values.
45 4Goes on.
mul20Multiplies 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 2+3×4=14.

TokenStack after itWhat happened
2 3 42 3 4Three values go on in turn.
mul2 12Multiplies the top 3 and 4. 2 stays below.
add14Adds 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.

  1. With parentheses, carry the inside over first.
  2. Write one operation as “left value, right value, operation word”: 𝑎−𝑏 is a b sub.
  3. Carry multiplication and division over before addition and subtraction, as in arithmetic.
  4. Treat a carried-over piece as one value again, and put it where the left or right value of the outer operation goes.

For (8−3)×(2+4) 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
TokenStack after itWhat happened
8 38 3The two values of the left bracket.
sub58−3.
2 45 2 4The two values of the right bracket. 5 waits below.
add5 62+4.
mul30Multiplies 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 10−3, and 3 10 lt asks “is 3<10?”.

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.

WordBeforeAfterWhat it does
dupaa aMakes one more of the top value.
dropa—Throws the top value away.
swapa bb aExchanges the top two values.
overa ba b aPuts one more of the second value on top.
nipa bbThrows 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.

TokenStack after itWhat happened
5 85 8Two values go on.
over5 8 5Copies the second value 5 to the top.
sub5 38−5; only the top two are used.
str.to_str io.println5Prints 3. 5 is left.
drop—Throws the remaining 5 away.

Common mistake: There is a rot that brings the third value to the top

There is not. Words that reach deep into the stack, like Forth’s 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:

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 an if 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

They are 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