10 Control

Syntax
control = "if" { body } [ "else" { body } ] "end"
        | "loop" [ "(" NAME ")" ] { body } "end"
        | "case" { branch } [ "otherwise" { body } "end" ] "end" ;
branch  = values "of" { body } "end" ;
values  = case-value | "(" case-value { "," case-value } ")" ;
leave   = ( "while" | "until" | "break" ) [ "(" NAME ")" ] | "return" ;
block   = "do" [ "(" effect ")" ] { body } "end" ;

(1) end closes the innermost open construct. The words that open one are word test record do if loop case of otherwise or_else defer. (A text literal and a note remark close on their own tag, 6.3, 6.5.)

(2) At load a processor compares the stacks of all paths. Where paths meet again, the stack shall be the same whichever path was taken. A leaving branch — one that ends with return, fail, break or a loop that never ends — is not compared.

10.1 Branches

(1) condition if body end takes the bool on top and runs the body when it is true. condition if body else
body end
runs the else body when it is false.

(2) The body of an if without else shall leave the stack as it found it. With else, the two bodies shall leave the same stack.

10-if.pnx

use io
word sign (n int gives str) n 0 lt if "negative" else n 0 eq if "zero" else "positive" end end end
-5 sign io.println
0 sign io.println
word warn (n int) n 100 gt if "big" io.println end end      rem no else: the branch leaves the stack as it was
500 warn

Output

$ pnx 10-if.pnx
negative
zero
big
Constraints

(3) Two branches that leave different stacks are refused.

10-if-unequal.pnx

word f (n int gives int) n 0 lt if 1 2 else 3 end end

Refused

$ pnx 10-if-unequal.pnx
10-if-unequal.pnx:1:47: the two branches of this if leave different stacks

10.2 Loops

(1) loop body end repeats its body. Inside it, these words leave the loop:

Each may be used any number of times, directly in the body of the loop or inside a branch in it (if else case or_else). They leave the innermost loop.

(2) One turn shall leave the stack as it found it, and at every place where the loop is left the stack shall be as it was when the loop was entered. A result of a loop is kept in a local.

10-loop.pnx

use io
use str
word digits (n int gives int)            rem how many decimal digits
  1 to count_
  loop
    n 10 div to n
    n 0 eq until
    count_ 1 add to count_
  end
  count_
end
12345 digits str.to_str io.println
word first_square_over (limit int gives int)
  1 to i
  loop
    i i mul limit gt if break end
    i 1 add to i
  end
  i
end
50 first_square_over str.to_str io.println

Output

$ pnx 10-loop.pnx
5
8

(3) loop (NAME) names a loop. while (NAME), until (NAME) and break (NAME) leave everything up to that loop at once. The name is visible only inside the loop’s body and is not a value. It follows the rule against hiding (9.5).

10-loop-named.pnx

use io
use str
word find_pair (target int gives str)      rem the first a, b in 1..9 with a*b = target
  1 to a
  0 to b
  loop (rows)
    1 to b
    loop
      a b mul target eq until (rows)       rem leaves both loops at once
      b 1 add to b
      b 9 gt until
    end
    a 1 add to a
    a 9 gt if "none" return end
  end
  cat (a, " x ", b)
end
42 find_pair io.println

Output

$ pnx 10-loop-named.pnx
6 x 7

(4) A loop with no while, until or break that leaves it never ends. A loop that never ends counts as a leaving branch, so a word does not need results after it. An until of an inner loop does not count as leaving the outer one, but an until (outer name) written in the inner loop does.

10-forever.pnx

use io
use str
word first_multiple (n int, k int gives int)     rem a loop with no exit counts as leaving
  loop
    n k mod 0 eq if n return end
    n 1 add to n
  end
end
100 7 first_multiple str.to_str io.println

Output

$ pnx 10-forever.pnx
105
Constraints

(5) A turn that changes the stack is refused.

10-loop-net.pnx

word f (gives int)
  0
  loop
    1 add
    dup 10 gt until
    1
  end
end

Refused

$ pnx 10-loop-net.pnx
10-loop-net.pnx:7:3: one turn of a loop must leave the stack as it found it

(6) while, until or break outside a loop is refused. They cannot leave an outer loop from inside a do block either; in the block of each, each_index or times they leave that block (10.6 (4)).

10-while-outside.pnx

word f (n int) n 0 gt while end

Refused

$ pnx 10-while-outside.pnx
10-while-outside.pnx:1:23: while, until and break belong inside a loop

(7) Code right after a leaving word (return fail break) or a loop that never ends, in the same branch, can never be reached and is refused.

10-unreachable.pnx

word f (n int gives int) n return 1 add end

Refused

$ pnx 10-unreachable.pnx
10-unreachable.pnx:1:35: this code cannot be reached: the code before it always leaves

10.3 Choosing

(1) value case branches… end takes the int, narrow integer (7.6), err or str on top, compares it with the values of the branches and runs the body of the first equal one. The values of a branch are literals, consts of this source, error codes (err.range), or a constant or failure code another module exports (module.NAME, 8.14); several values go in parentheses with commas. When no branch matches, the otherwise body runs. Every branch shall leave the same stack.

10-case.pnx

use io
const quit_code 9
word describe (cmd str gives str)
  cmd case
    "add" of "adds a line" end
    ("rm", "del") of "deletes a line" end
    otherwise "unknown command" end
  end
end
word level (n int gives str)
  n case
    0 of "off" end
    1 of "low" end
    quit_code of "quit" end
    otherwise "high" end
  end
end
"del" describe io.println
"mv" describe io.println
9 level io.println

Output

$ pnx 10-case.pnx
deletes a line
unknown command
quit
Constraints

(2) The same value in two branches is refused.

10-case-twice.pnx

word f (n int gives int) n case 1 of 10 end 1 of 20 end otherwise 0 end end end

Refused

$ pnx 10-case-twice.pnx
10-case-twice.pnx:1:45: this value is already in an earlier branch of the case

(3) A constant of a module (a path such as json.key or err.full) cannot be a branch value; compare such values with eq.

10-case-path.pnx

use json
word f (k int gives bool) k case json.key of true end otherwise false end end end

Refused

$ pnx 10-case-path.pnx
10-case-path.pnx:2:34: a case value is a literal or the name of a const
Faults

(4) With no otherwise and no matching branch, the fault is range.

10-case-nomatch.pnx

use io
word f (n int gives str) n case 1 of "one" end 2 of "two" end end end
2 f io.println
3 f io.println

Fault

$ pnx 10-case-nomatch.pnx
two
10-case-nomatch.pnx:2: index out of range
  n int = 3

10.4 Returning

(1) return ends the word on the spot. The stack there shall match the results (9.2 (4)).

Constraints

(2) return is used only inside a definition (a word, a block value, a test). In code outside definitions it is refused.

10-return-outside.pnx

use io
"start" io.println
return

Refused

$ pnx 10-return-outside.pnx
10-return-outside.pnx:3:1: return belongs inside a definition

10.5 Blocks

(1) do … end is a block. A block is used in two ways; the form is the same, and what takes the block decides.

(2) A signature may follow do; its names become locals of the block, as in a word definition. When what takes the block fixes its effect, the signature may be left out.

10-inline-locals.pnx

use io
use str
word total_over (limit int gives int)
  0 to acc
  1 10 range do (i int) i limit gt if acc i add to acc end end each   rem the block sees acc and limit
  acc
end
5 total_over str.to_str io.println

Output

$ pnx 10-inline-locals.pnx
30

10-value-block.pnx

use io
use str
word twice (n int, f word (int gives int) gives int) n f call f call end
5 do (x int gives int) x 3 mul end twice str.to_str io.println
word add_one (int gives int) 1 add end
word demo (gives int)
  do (x int gives int) x add_one end to step     rem a block value in a local
  41 step call
end
demo str.to_str io.println

Output

$ pnx 10-value-block.pnx
45
42

(2a) A block value with no signature whose body is one word defined with word, of this source or exported by a module in use (do by_len end, do geo.area end), is that word itself: its type is word with the word’s signature, and call calls the word. Any other word is given in a block with the block’s signature; inputs given as types only stay on the stack (9.2 (2)), so the block’s body is the word alone: do (str, str gives bool) str.starts_with end.

10-word-value.pnx

use io
use str
word by_len (a str, b str gives bool) a str.len b str.len lt end
word shorter (f word (str, str gives bool), a str, b str gives str) a b f call if a else b end end
word demo ()
  do by_len end to order                          rem the word by_len itself: word (str, str gives bool)
  cat ("shorter: ", order "pear" "fig" shorter) io.println
  do (str, str gives bool) str.starts_with end to starts    rem a word of the language, with its signature
  "banana" "ban" starts call if "starts with ban" io.println end
end
demo

Output

$ pnx 10-word-value.pnx
shorter: fig
starts with ban
Constraints

(3) A block value that uses a local from outside is refused (the name is not declared inside the block).

10-value-block-outer.pnx

word twice (n int, f word (int gives int) gives int) n f call f call end
word scale (k int gives int) 5 do (x int gives int) x k mul end twice end

Refused

$ pnx 10-value-block-outer.pnx
10-value-block-outer.pnx:2:55: this name is not declared before this place

(4) A block expanded into a loop (each, times and so on) shall leave the stack as it found it.

10-block-net.pnx

array xs int 3
word f (gives int) 0 xs do (x int) x end each end

Refused

$ pnx 10-block-net.pnx
10-block-net.pnx:2:38: this block must leave the stack as it found it

(4a) A block value with no signature takes nothing, so a word in it that needs values is refused, and the message shows both forms of (2a).

10-word-value-bare.pnx

use str
word demo ()
  do str.starts_with end drop
end

Refused

$ pnx 10-word-value-bare.pnx
10-word-value-bare.pnx:3:6: a block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end

(4b) A word declared with forward is given as a value only after its definition.

10-word-value-forward.pnx

forward word later (n int)
word demo ()
  do later end drop
end
word later (n int) end

Refused

$ pnx 10-word-value-forward.pnx
10-word-value-forward.pnx:3:6: a word declared with forward is given as a value only after its definition

10.6 Stems and combining words

(1) A stem yields values one after another. A stem is not a value: it cannot be stored, and the combining word right after it shall take it. Together they make one loop. These make stems:

StemElements
the name of an array or listthe cells’ values (rows, for records); a list up to its length. Queues, buffers and indexes are not stems
the name of a poolits live rows, in the order of their slots; for each only (8.9 (3a))
lo hi rangethe ints from lo up to but not including hi
s sep str.split, s str.lines, s str.words, s str.charsstr views (16.1); chars one UTF-8 character each
s str.bytesu8: the bytes (16.1)
s pat ptn.matches, s re rex.matches, s re rex.splitthe parts matched (16.6, 16.7)
s csv.fieldsthe fields (16.9)

(2) The combining words are below; the “block” column is what the block takes and gives.

WordBlockResult
each(T)none
each_index(T, i int)none
fold(acc U, T gives U)U; the first value goes before the stem: 0 xs do … end fold
count(T gives bool)int
any all(T gives bool)bool; stops early once the answer is known
first(T gives bool)T or fail; err.not_found when none
sumnoneint (of a stem of dec(N), a dec(N)); an overflow is the fault overflow
minimum maximumnoneint or fail (dec(N) likewise); err.not_found for an empty stem
map_into(T gives U)a destination after it; or fail
filter_into(T gives bool)a destination after it; or fail
n times()none; no stem, n times

10-combinators.pnx

use io
use str
array scores int 6
assign scores (72, 95, 88, 61, 90, 79)
scores sum str.to_str io.println
scores do (s int gives bool) s 90 ge end count str.to_str io.println
scores do (s int gives bool) s 60 lt end any if "someone failed" else "all passed" end io.println
0 scores do (acc int, s int gives int) acc s max end fold str.to_str io.println
scores do (s int, i int) i 2 lt if cat ("#", i, " ", s) io.println end end each_index
1 5 range do (i int) i str.to_str io.print end each
"" io.println
3 do "x" io.print end times
"" io.println

Output

$ pnx 10-combinators.pnx
485
2
all passed
95
#0 72
#1 95
1234
xxx

10-first.pnx

use io
use str
array xs int 5
assign xs (3, 8, 12, 5, 20)
xs do (x int gives bool) x 10 gt end first or_else (e) -1 end str.to_str io.println
xs do (x int gives bool) x 100 gt end first or_else (e) -1 end str.to_str io.println
xs minimum or_else (e) 0 end str.to_str io.println

Output

$ pnx 10-first.pnx
12
-1
3

(3) map_into and filter_into make no new storage; they write into the destination. A list destination is pushed onto and nothing is given. An array destination is written from cell 0, and the number of cells written is given (gives n int or fail). When the destination is full, they fail with err.full; what was written stays. The destination may be a collection parameter (9.9).

10-map-into.pnx

use io
use str
array xs int 4
assign xs (1, 2, 3, 4)
list squares int 4
array evens int 4
word go (or fail)
  xs do (x int gives int) x x mul end squares map_into or_fail
  xs do (x int gives bool) x 2 mod 0 eq end evens filter_into or_fail to n
  cat ("squares ", 3 squares idx, ", evens written ", n) io.println
end
go or_else (e) e err_text io.println end

Output

$ pnx 10-map-into.pnx
squares 16, evens written 2

(4) A stem and its combining word make one loop, and in the block of each, each_index or times, break leaves it: the stem stops and the code after the combining word runs. while and until leave it as they leave a loop (10.2 (1)). The rules of leaving a loop hold: the stack is as it was when the block began (10.2 (2)), and the block’s deferred code runs (10.7). A break in a loop inside the block leaves that loop. The blocks of the other combining words, which give a result made from every element, are not left early, and a named loop outside the block is not left from inside it.

10-leave-each.pnx

use io
use str
word until_blank (s str)
  s str.lines do (line str)
    line str.len 0 eq if break end
    cat ("> ", line) io.println
  end each
  "done" io.println
end
"first\nsecond\n\nnot read" until_blank

Output

$ pnx 10-leave-each.pnx
> first
> second
done

10.7 Deferred code

Syntax
defer = "defer" { body } "end" ;

(1) defer CODE end writes code that runs when the enclosing block is left. The block is a word’s body, a loop’s body (at every turn) or the do block of a combining word (at every run). The code runs on every way out of that block: the block’s end, the end of a turn, break, a while or until that leaves the loop, return, or_return, or_fail and fail. Several run in the reverse of the order they are written. They do not run when a fault ends the run (the state closes the handles then, 19.6).

10-defer.pnx

use io
use str
buffer trace 256
word say (s str) s trace append or_else (e) end end
rem Every way out of a block runs its defers, the last written first.
word step (n int or fail)
  "open " say
  defer "close " say end
  defer "flush " say end
  n 0 lt if err.range fail end
  "work " say
end
1 step or_else (e) end
-1 step or_else (e) "(failed) " say end
trace contents str.trim io.println

Output

$ pnx 10-defer.pnx
open work flush close open flush close (failed)

(2) When deferred code gives an owned value (m release, f file.close), the defer takes that value over. It counts as given on every way out of the block, so no failure path needs code of its own to close it. Inside the block the value can still be lent.

10-defer-owned.pnx

use io
use str
buffer report 4096
rem A mark taken back at the end of every turn, also when append fails.
word build (or fail)
  1 50 range do (i int)
    mark to m
    defer m release end
    cat ("row ", i, ": ", i i mul, "\n") report append or_fail
  end each
end
build or_else (e) cat ("full: ", e err_text) io.println end
cat ("report: ", report len, " bytes") io.println

Output

$ pnx 10-defer-owned.pnx
report: 585 bytes
Constraints

(3) defer is written right in a word’s body, a loop’s body or a do block. In a branch of if, case or or_else, or outside a definition, it is refused.

10-defer-branch.pnx

word f (c bool)
  c if defer end end
end

Refused

$ pnx 10-defer-branch.pnx
10-defer-branch.pnx:2:8: defer is written right in a word's body, a loop's body or a do block, not in a branch

(4) Deferred code leaves the stack as it found it and leaves nothing on it. It cannot hold return fail break while until to set loop do case defer, or or_fail or_return; a word that can fail is taken with or_else inside it.

(5) Giving an owned value that a defer took over, inside the block, is refused.

10-defer-taken.pnx

word twice (gives int)
  mark to m
  defer m release end
  m release
  0
end

Refused

$ pnx 10-defer-taken.pnx
10-defer-taken.pnx:4:5: a defer gives this owned value when the block is left; here it can only be lent
Diagnostics
ConditionMessage
Two branches leave different stacksthe two branches of this if leave different stacks
A turn changes the stackone turn of a loop must leave the stack as it found it
while until break outside a loopwhile, until and break belong inside a loop
while until break in the block of a word that gives a resultonly the block of each, each_index or times is left early; this word gives a result made from every element
break (NAME) to a loop outside a do blocka named loop outside this block is not left from inside it; break leaves the block
Unreachable codethis code cannot be reached: the code before it always leaves
The same value twice in a casethis value is already in an earlier branch of the case
An expanded block changes the stackthis block must leave the stack as it found it
return outside a definitionreturn belongs inside a definition
A branch value that is not a constanta case value is a literal or the name of a const
A combining word on something that is not a stemonly an array, a list or a pool is a stem (not a queue, a buffer or an index)
defer in a branchdefer is written right in a word's body, a loop's body or a do block, not in a branch
defer outside a definitiondefer belongs inside a definition
Deferred code changes the stacka defer leaves the stack as it found it
A word that leaves, in deferred codea defer holds no return, fail, break, while, until, to, set, loop, do, case or defer
or_fail or_return in deferred codea defer does not leave the word; a failure in it is taken with or_else
Giving a value a defer took overa defer gives this owned value when the block is left; here it can only be lent
Too few values in a block value with no signaturea block without a signature takes nothing; a word of the program is given as do NAME end, any other word with the block's signature: do (str, str gives bool) str.starts_with end
A forward word as a value before its definitiona word declared with forward is given as a value only after its definition

10.8 Ends of statements

(1) A comma or a dot written in code (not inside the parentheses of a signature, of cat or of a list of names) ends a statement: the code since the last comma or dot. Neither changes the stack, and neither is required: code without them means what it meant before.

(2) A comma waits for nothing: the effects of the statement may still be under way when the next one starts.

(3) A dot is a barrier: every effect begun before it is finished before anything after it. The processor’s part is to call the function the host gave with pinax_on_barrier (19.4 (3)); the host’s part is to write what its host words hold back (standard output, a file’s writes) and to wait for what they started in the background. The end of the code of a load and the end of a session line are a dot, also when they end with a fault. In a source loaded as data the two mark statements and call nothing.

(4) An effect whose result the statement needs (a read, a command’s output) is done when it is reached. Between two dots, effects on the same resource keep the order of the program; effects on different resources may overlap. This edition runs every effect in the order of the program; the overlap is the freedom a later edition may use.

10-statements.pnx

use io
word show (n int)
  cat ("square: ", n n mul) io.println .
end
"start" io.println ,
3 show ,
4 show .

Output

$ pnx 10-statements.pnx
start
square: 9
square: 16