4 Locals and flow

Builds on: Chapter 3 (signatures, named inputs).

Questions this chapter answers

4.1 Locals

In a body, to NAME puts the top value into that name. The first time a name is used, a local of the value’s type is made; there is no separate line that declares it.

04-locals.pnx

use io
word describe (a int, b int, c int gives str)
  a b add c add to total
  total 3 div to mean
  a b max c max to top
  cat ("total ", total, ", mean ", mean, ", largest ", top)
end
3 9 6 describe io.println

Output

$ pnx 04-locals.pnx
total 18, mean 6, largest 9

to takes the top value off the stack and keeps it in the name. Using the name puts the value back on. Instead of holding values on the stack for long, keep them in names and take them out when needed.

TokenStack after itWhat happened
a b add12a is 3, b is 9.
c add18c is 6.
to total—Takes 18 off and keeps it in a new local total. The stack is empty.
total 3 div6Using total puts 18 on; it is divided by 3.
to mean—Keeps 6 in mean.

A local is visible from where it is first stored to the end of the branch, loop or block around it. A name first made inside a branch cannot be read outside it: coming by the other branch, it would hold no value.

04-scope.pnx

use io
word f (n int gives int)
  n 0 gt if n 2 mul to doubled end
  doubled
end

Refused

$ pnx 04-scope.pnx
04-scope.pnx:4:3: this name is not declared before this place

A global is stored with set, not to; to stores only into locals.

04-store-global.pnx

use io
global total int
word add_to_total (n int) total n add set total end     rem set: a global is written with set
3 add_to_total
4 add_to_total
cat ("total ", total) io.println

Output

$ pnx 04-store-global.pnx
total 7

Common mistake: Storing into a global with to total

to with the name of a global is refused, and the message names set. A global cannot be hidden, so no local of that name can be made; the rule also keeps a to meant to make a local from changing a global silently.

04-store-global-bare.pnx

global total int
word add_to_total (n int) total n add to total end

Refused

$ pnx 04-store-global-bare.pnx
04-store-global-bare.pnx:2:42: this name is a global; a global is written with set: value set name

4.2 Branches

condition if … else … end looks at the bool on top and runs one side. Both branches shall leave the same stack; a branch that leaves with return does not need to match.

04-if.pnx

use io
word grade (score int gives str)
  score 90 ge if "A" return end
  score 80 ge if "B" return end
  score 70 ge if "C" else "F" end
end
95 grade io.println
82 grade io.println
40 grade io.println

Output

$ pnx 04-if.pnx
A
B
F

if takes the bool off the top and chooses the way. The two branches have to leave the same stack because the code after end does not know which branch it came from. Whichever way it came, the stack must be the same for the processor to check what follows.

04-sign.pnx

use io
word sign (n int gives str)
  n 0 lt if                     rem lt leaves a bool; if takes it off the stack
    "negative"                  rem this way leaves one str
  else
    n 0 eq if "zero" else "positive" end   rem and so does this way, whichever inner way it takes
  end                           rem both ways meet here with one str on the stack
end
-4 sign io.println
0 sign io.println
9 sign io.println

Output

$ pnx 04-sign.pnx
negative
zero
positive
TokenStack after itWhat happened
n 0 lttrueWith n at −4 the comparison leaves true.
if—Takes the bool off and goes to the first branch.
"negative""negative"This branch leaves one str.
end"negative"The else branch also leaves one str, so the two ways agree.

An if without else shall leave the stack as it was. Leaving out the else of an if that leaves a value is refused.

04-if-unequal.pnx

use io
word label (n int gives str) n 0 lt if "negative" end end

Refused

$ pnx 04-if-unequal.pnx
04-if-unequal.pnx:2:51: an if without else must leave the stack as it found it

4.3 Loops

loop … end repeats. Inside it, condition until leaves when the condition is true, condition while when it is false, and break without a condition. One turn shall leave the stack as it found it — so the values a loop makes are gathered in locals.

04-loop.pnx

use io
word collatz (n int gives int)              rem steps to reach 1
  0 to steps
  loop
    n 1 eq until
    n 2 mod 0 eq if n 2 div else n 3 mul 1 add end to n
    steps 1 add to steps
  end
  steps
end
cat ("27 takes ", 27 collatz, " steps") io.println

Output

$ pnx 04-loop.pnx
27 takes 111 steps

One turn leaves the stack as it found it for the same reason. The head of the loop sees the stack both when the loop is first entered and when a turn comes back, and both must be the same. If each turn piled up one more value, the processor, which cannot know how many turns there will be, could not fix the depth of the stack. So the values a loop makes are gathered in locals.

04-loop-sum.pnx

use io
use str
word sum_to (n int gives int)
  0 to total                    rem what the loop builds lives in locals, not on the stack
  1 to i
  loop
    i n gt until                rem leave once i passes n
    total i add to total        rem the stack is empty again at the end of the turn
    i 1 add to i
  end
  total
end
10 sum_to str.to_str io.println

Output

$ pnx 04-loop-sum.pnx
55

04-loop-leaves.pnx

word count_up (n int gives int)
  0 to i
  loop
    i n ge until
    i 1 add                     rem a new value each turn, and nothing takes it
  end
  i
end

Refused

$ pnx 04-loop-leaves.pnx
04-loop-leaves.pnx:6:3: one turn of a loop must leave the stack as it found it

count_up makes a value with i 1 add every turn, and nothing takes it. With to i after it the value goes into i and the stack is as it was at the start of the turn.

A named loop can be left all at once from an inner loop.

04-named-loop.pnx

use io
word is_prime (n int gives bool)
  n 2 lt if false return end
  2 to d
  loop
    d d mul n gt if true return end
    n d mod 0 eq if false return end
    d 1 add to d
  end
end
word first_pair (target int gives str)       rem the first a < b <= 20, both prime, with a + b = target
  2 to a
  0 to b
  loop (outer)
    a 1 add to b
    loop
      b 20 gt until
      a is_prime b is_prime and a b add target eq and until (outer)
      b 1 add to b
    end
    a 1 add to a
    a 20 gt if "none" return end
  end
  cat (a, " + ", b)
end
26 first_pair io.println

Output

$ pnx 04-named-loop.pnx
7 + 19

Q&A

Q. Can I give the number of loops to leave instead?

A. No. With a number, wrapping a loop in one more would quietly leave another loop. A name cannot do that.

4.4 Choosing

case compares an int or str with the values of its branches. Several values for one branch go in parentheses. What to do when nothing matches goes in otherwise; with no otherwise and no match, it is a fault.

04-case.pnx

use io
word day_kind (day str gives str)
  day case
    ("sat", "sun") of "weekend" end
    ("mon", "tue", "wed", "thu", "fri") of "weekday" end
    otherwise "not a day" end
  end
end
"sun" day_kind io.println
"wed" day_kind io.println
"xyz" day_kind io.println

Output

$ pnx 04-case.pnx
weekend
weekday
not a day

The values of the branches are literals or consts of this source. Constants of modules such as err.full or json.key cannot be case values; compare them with eq.

4.5 Stems and blocks

A loop over an array or a list is written with a stem and a combining word instead. The name of the array is the stem, and a do … end block after it, with a word such as each, count or fold, takes it. The block is expanded in place, so it can use the locals around it.

04-stems.pnx

use io
use str
array temps int 7
assign temps (12, 15, 9, 21, 18, 7, 14)
cat ("sum ", temps sum, ", above 13: ", temps do (t int gives bool) t 13 gt end count) io.println
cat ("max ", temps maximum or_else (e) 0 end, ", min ", temps minimum or_else (e) 0 end) io.println
temps do (t int, i int) cat ("day ", i, ": ", t) io.println end each_index
word spread (gives int) 0 temps do (acc int, t int gives int) acc t 10 sub abs add end fold end
cat ("distance from 10, added up: ", spread) io.println

Output

$ pnx 04-stems.pnx
sum 96, above 13: 4
max 21, min 7
day 0: 12
day 1: 15
day 2: 9
day 3: 21
day 4: 18
day 5: 7
day 6: 14
distance from 10, added up: 34

lo hi range is a stem too (hi is not included). fold takes a first value before the stem, and its block takes (the value so far, an element) and gives the new value.

The block of each, each_index or times is left early with break, while or until, as a loop is: the stem stops and the code after the word runs. A search that stops at the first hit is written so.

04-leave-each.pnx

use io
array prices int 6
word load_prices ()
  0 6 range do (i int) i 7 mul 3 add i set prices end each
end
word first_over (limit int gives r int)
  -1 to found
  prices do (p int, i int)
    p limit gt if i to found break end
  end each_index
  found
end
load_prices
cat ("first over 20 at ", 20 first_over) io.println
cat ("first over 99 at ", 99 first_over) io.println

Output

$ pnx 04-leave-each.pnx
first over 20 at 3
first over 99 at -1

4.6 Words as values

A block that is not given to a combining word is a value: it can be kept in a local, returned, or given to a parameter of type word (signature), and call calls it. Such a block sees no locals around it. To give a word of the program as a value, write do NAME end with no signature: the value is that word, with its signature.

04-word-values.pnx

use io
use str
word up (a int, b int gives bool) a b lt end
word down (a int, b int gives bool) a b gt end
word best (before word (int, int gives bool), x int, y int gives int) y x before call if y else x end end
word order_named (how str gives word (int, int gives bool) or fail)
  how case
    "up" of do up end end
    "down" of do down end end
    otherwise err.not_found fail end
  end
end
word demo (or fail)
  "up" order_named or_fail to smallest_first
  "down" order_named or_fail to largest_first
  cat ("smallest ", smallest_first 7 3 best, ", largest ", largest_first 7 3 best) io.println
end
demo or_else (e) e err_text io.println end

Output

$ pnx 04-word-values.pnx
smallest 3, largest 7

Q&A

Q. How is a word of the language, such as str.starts_with, given as a value?

A. With the block’s signature written as types only: do (str, str gives bool) str.starts_with end. Inputs written as types stay on the stack, so the word takes them as it is. Without a signature the block takes nothing, and the message shows both forms.

Common mistake: A case branch can hold the word itself: "prefix" of str.starts_with end

A branch runs its words; it does not make a value of them. There str.starts_with is called, and finds too few values on the stack. Make the value with a block: "prefix" of do (str, str gives bool) str.starts_with end end, or do NAME end for a word of the program.

04-word-values-branch.pnx

use str
word test_named (how str gives word (str, str gives bool) or fail)
  how case
    "prefix" of str.starts_with end
    otherwise err.not_found fail end
  end
end

Refused

$ pnx 04-word-values-branch.pnx
04-word-values-branch.pnx:4:17: the stack does not hold enough values for this word

Q&A

Q. What if the two branches of if should leave values of different types?

A. They cannot. The code after if does not know which branch ran, so both leave the same number of values of the same types. When one branch has a value and the other has not, that is a failure (chapter 8) or a row asked with is_none (chapter 7).

Summary