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 runs the
body endelse 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:
condition while— leaves when the condition is false;condition until— leaves when the condition is true;break— leaves without a condition.
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.
- An inline block — a block given directly to a combining word (10.6). It is expanded in place, so it can read and store the locals around it, and a
returninside it leaves the word around it. - A block value — a block stored in a local, or given to a parameter of type
word (signature). It becomes a nameless word, called withcall. It does not see the locals around it (it captures nothing); it sees globals.
(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:
| Stem | Elements |
| the name of an array or list | the cells’ values (rows, for records); a list up to its length. Queues, buffers and indexes are not stems |
| the name of a pool | its live rows, in the order of their slots; for each only (8.9 (3a)) |
lo hi range | the ints from lo up to but not including hi |
s sep str.split, s str.lines, s str.words, s str.chars | str views (16.1); chars one UTF-8 character each |
s str.bytes | u8: the bytes (16.1) |
s pat ptn.matches, s re rex.matches, s re rex.split | the parts matched (16.6, 16.7) |
s csv.fields | the fields (16.9) |
(2) The combining words are below; the “block” column is what the block takes and gives.
| Word | Block | Result |
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 |
sum | none | int (of a stem of dec(N), a dec(N)); an overflow is the fault overflow |
minimum maximum | none | int 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
| Condition | Message |
|---|---|
| Two branches leave different stacks | the two branches of this if leave different stacks |
| A turn changes the stack | one turn of a loop must leave the stack as it found it |
while until break outside a loop | while, until and break belong inside a loop |
while until break in the block of a word that gives a result | only 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 block | a named loop outside this block is not left from inside it; break leaves the block |
| Unreachable code | this code cannot be reached: the code before it always leaves |
The same value twice in a case | this value is already in an earlier branch of the case |
| An expanded block changes the stack | this block must leave the stack as it found it |
return outside a definition | return belongs inside a definition |
| A branch value that is not a constant | a case value is a literal or the name of a const |
| A combining word on something that is not a stem | only an array, a list or a pool is a stem (not a queue, a buffer or an index) |
defer in a branch | defer is written right in a word's body, a loop's body or a do block, not in a branch |
defer outside a definition | defer belongs inside a definition |
| Deferred code changes the stack | a defer leaves the stack as it found it |
| A word that leaves, in deferred code | a defer holds no return, fail, break, while, until, to, set, loop, do, case or defer |
or_fail or_return in deferred code | a defer does not leave the word; a failure in it is taken with or_else |
| Giving a value a defer took over | a 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 signature | 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 |
| A forward word as a value before its definition | a 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