9 Words
9.1 Definitions
Syntax
definition = [ "export" ] "word" NAME "(" effect ")" { body } "end" ;
effect = [ params ] [ "gives" params ] [ "or" "fail" ] ;
params = param { "," param } ;
param = [ NAME ] ( type | collection-type ) ; (* collection-type: 9.9 *)
type = [ "owned" ] ( NAME [ "(" size ")" ] | "word" "(" effect ")" ) ;(1) word NAME (signature) body end defines a word. Words are defined only outside definitions, at the outermost level of a source; a word cannot be defined inside another.
(2) To call a word is to write its name in a body. Where it is called, the topmost values of the stack become its inputs, and when it ends its results are in their place. Core words, words of standard modules and host words are called the same way.
9.2 Signatures
(1) Every word has a signature: its inputs, then gives and its results, then or fail if it can fail. A word with neither inputs nor results has (). Signatures are checked at load: the stack where a word is called shall match its inputs, and the stack where its body ends shall match its results.
(2) An input with a name (w int) becomes a local of that name, and the body starts with an empty stack. An input given as a type only (int) stays on the stack when the body starts. Names on the results side are for the reader only and make no locals. An input may also take a collection (9.9).
09-signatures.pnx
use io
use str
word area (w int, h int gives a int) w h mul end rem named inputs become locals
word square (int gives int) dup mul end rem a bare type: the value stays on the stack
global total int
word reset () 0 set total end rem neither inputs nor results
3 4 area str.to_str io.println
5 square str.to_str io.println
reset
Output
$ pnx 09-signatures.pnx
12
25
Constraints
(3) The inputs of a signature shall be either all named or all bare types.
09-signature-mixed.pnx
word area (w int, int gives int) w mul end
Refused
$ pnx 09-signature-mixed.pnx
09-signature-mixed.pnx:1:32: inputs are all named or all bare types, not a mix
(4) Where the body ends (and at every return) the stack shall hold exactly the results, in order and type. More or fewer values are refused.
09-result-missing.pnx
word half (n int gives r int) n 2 div drop end
Refused
$ pnx 09-result-missing.pnx
09-result-missing.pnx:1:44: the stack does not hold exactly the results of the signature here
9.3 The stack
(1) Values passed between words lie on the stack. A literal or a name pushes one value; a word takes its inputs off and pushes its results. At load a processor knows, at every place of a body, the depth of the stack and the type of each slot.
(2) Only five core words rearrange the stack.
| Word | Before → after | Meaning |
dup | a → a a | one more of the top |
drop | a → | discard the top |
swap | a b → b a | exchange the top two |
over | a b → a b a | one more of the second, on top |
nip | a b → b | discard the second |
There are no deeper words (rot pick roll depth). When three or more values are involved, use locals.
09-stack-words.pnx
use io
use str
word show (a int, b int) cat (a, " ", b) io.println end
1 2 swap show rem 2 1
1 2 over drop show rem over copies the second: 1 2 1, drop gives 1 2
1 2 3 nip show rem nip drops the second: 1 3
4 dup show rem 4 4
Output
$ pnx 09-stack-words.pnx
2 1
1 2
1 3
4 4
Note
For a reader new to a stack, here is 2 3 add 4 mul one token at a time (the top on the right).
| Token | Stack after it | What happened |
|---|---|---|
2 | 2 | A value goes on. |
3 | 2 3 | A value goes on. |
add | 5 | Two values come off; their sum goes on. |
4 | 5 4 | A value goes on. |
mul | 20 | Two values come off; their product goes on. |
At load the processor knows the second column of this table (depth and types) at every place in advance. That is why the mistakes of (3) and (3a) show before anything runs.
Constraints
(3) A word given fewer values than it takes is refused. The depth of the stack does not change while running.
09-stack-short.pnx
word f (int gives int) add end
Refused
$ pnx 09-stack-short.pnx
09-stack-short.pnx:1:24: the stack does not hold enough values for this word
(3a) When the code outside definitions (5.2) ends, at the end of the source, values left on the stack are refused. The stack at the end of a word’s body is 9.2′s.
09-leftover.pnx
use io
"done" io.println
1 2 add rem a value that nothing takes
Refused
$ pnx 09-leftover.pnx
09-leftover.pnx:4:1: values are left on the stack at the end of the source
(4) Using a name that is not defined is refused.
09-unknown-word.pnx
3 4 plus drop
Refused
$ pnx 09-unknown-word.pnx
09-unknown-word.pnx:1:5: this name is not declared before this place
9.4 Locals
Syntax
store = "to" ( NAME | PATH | "(" NAME { "," NAME } ")" ) ;
set = "set" ( NAME | PATH ) ;(1) In a body, to NAME takes the top value off and puts it in that name. When the name is new, a local variable of the value’s type is made; later tos shall store the same type. to stores only into locals (and into fields, with to r.FIELD).
(1a) A global is stored with set NAME: v set NAME for a scalar global, v i set NAME for a cell of an array or a list. A global another module exports is set MODULE.NAME. Inside and outside definitions alike. The rule makes the text show whether a store makes a local or changes a global.
(2) to (a, b, c) stores the top three values at once. The rightmost name takes the top value — the same order as in a signature.
09-locals.pnx
use io
use str
word stats (a int, b int, c int gives s str)
a b add c add to total rem the first `to` makes a local of that type
total 3 div to mean
a b max c max to top
cat ("total ", total, ", mean ", mean, ", max ", top)
end
word split3 (gives int, int, int) 1 2 3 end
3 9 6 stats io.println
word show3 () split3 to (x, y, z) cat (x, y, z) io.println end rem the rightmost is the top
show3
Output
$ pnx 09-locals.pnx
total 18, mean 6, max 9
123
09-store-global.pnx
use io
use str
global calls int
word tick () calls 1 add set calls end rem set stores into a global; to only into locals
tick tick tick
calls str.to_str io.println
Output
$ pnx 09-store-global.pnx
3
(3) A local is visible from where it is first stored to the end of the branch, loop or block around it. A local made outside can be stored into inside.
Constraints
(4) Storing a value of another type into a local is refused.
09-local-type.pnx
word f (gives s str)
5 to v
"five" to v
v
end
Refused
$ pnx 09-local-type.pnx
09-local-type.pnx:3:13: this local variable has another type than the value stored into it
(5) A local first made inside a branch cannot be read outside it: coming by the other branch, it would hold no value.
09-local-scope.pnx
word f (flag bool gives int)
flag if 1 to v end
v
end
Refused
$ pnx 09-local-scope.pnx
09-local-scope.pnx:3:3: this name is not declared before this place
(6) to with the name of a global is refused. A global cannot be hidden (9.5), so no local of that name can be made, and the global is not changed silently either; the message names set. set with a name that is not a global is refused.
09-store-global-bare.pnx
global total int
word add_up (n int) total n add to total end rem total is a global: set total
Refused
$ pnx 09-store-global-bare.pnx
09-store-global-bare.pnx:2:36: this name is a global; a global is written with set: value set name
9.5 No hiding
(1) A new name (a parameter, a local, the name of a loop) cannot be the same as a name visible there — a word, a global, a constant, a type, a local outside. One name means one thing at one place.
Constraints
(2) A parameter or local with the name of a visible name is refused.
09-shadow.pnx
global limit int
word clip (n int, limit int gives int) n limit min end
Refused
$ pnx 09-shadow.pnx
09-shadow.pnx:2:40: this name is already visible here; a new name may not hide it
09-shadow-word.pnx
word f (add int gives int) add 1 add end
Refused
$ pnx 09-shadow-word.pnx
09-shadow-word.pnx:1:28: add is a word of the language; choose another name
(3) A word of the language — a reserved word, a core word, a word of clause 15 or 16 written without a module, a type — is never a new name: not of a word, a declaration, a parameter, a local or a loop. A reserved word is not the name of a field either; another word of the language may be, since a field is always written after its record (8.8). The message says which word it is.
09-name-taken.pnx
word swap (a int, b int gives b int, a int) b a end
Refused
$ pnx 09-name-taken.pnx
09-name-taken.pnx:1:6: swap is a word of the language; choose another name
9.6 Recursion and tail calls
(1) A word may call itself. No forward declaration is needed.
(2) A tail call — a call that is the last thing a word does — reuses the caller’s place. Recursion through tail calls does not grow the depth. It needs no mark.
09-recursion.pnx
use io
use str
word fact (n int gives int) n 1 le if 1 else n n 1 sub fact mul end end
word count_down (n int, acc int gives int) rem a call in tail place reuses the frame
n 0 eq if acc return end
n 1 sub acc 1 add count_down
end
20 fact str.to_str io.println
1000000 0 count_down str.to_str io.println
Output
$ pnx 09-recursion.pnx
2432902008176640000
1000000
Faults
(3) Calls that are not tail calls pile up; past the depth limit (annex C) they stop with the fault depth.
09-depth.pnx
word deep (n int gives int) n 0 eq if 0 else n 1 sub deep 1 add end end
1000000 deep drop
Fault
$ pnx 09-depth.pnx
09-depth.pnx:1: call depth limit
n int = 983617
9.7 Forward words
(1) forward word NAME (signature) announces a word defined further on, for two words that call each other.
09-forward-word.pnx
use io
forward word is_odd (n int gives bool)
word is_even (n int gives bool) n 0 eq if true else n 1 sub is_odd end end
word is_odd (n int gives bool) n 0 eq if false else n 1 sub is_even end end
7 is_even if "7 is even" else "7 is odd" end io.println
Output
$ pnx 09-forward-word.pnx
7 is odd
Constraints
(2) The definition shall have the announced signature, in the same module; otherwise it is refused.
09-forward-other.pnx
forward word f (n int gives int)
word f (n int gives bool) n 0 eq end
Refused
$ pnx 09-forward-other.pnx
09-forward-other.pnx:2:27: the definition has another signature than its forward declaration
9.8 Joining text
Syntax
cat = "cat" "(" phrase { "," phrase } ")" ;
phrase = body { body } ; (* code that leaves one value *)(1) cat ( … ) joins the comma-separated items into one str. Each item is code that leaves one value. A str goes in as it is, an int in decimal, a bool as true or false. Other forms (hexadecimal and so on) are made into a str first by the words that make them (16.2).
(2) The result is a view of bytes made in the region (12.2). The number of items is what the text shows; the processor adds up the length first and writes once.
09-cat.pnx
use io
use fmt
word show (id int, name str, ok bool) cat ("id=", id, " name=", name, " ok=", ok, " hex=", id fmt.hex) io.println end
255 "kim" true show
Output
$ pnx 09-cat.pnx
id=255 name=kim ok=true hex=ff
9.9 Collections as parameters
Syntax
collection-type = ( "array" | "list" | "queue" ) cell | "buffer" ;
cell = NAME [ "(" size ")" ] ; (* int, bool, err, dec(N), str(N), a record, or a table: its rows *)(1) An input of type array T, list T or queue T, or of type buffer, takes a collection: the caller writes the collection’s name there (scores total). In the body the parameter is used wherever the name of such a collection goes — l len, i l idx, v l push, v i set l, l do … end each, … end l map_into, q take, s b append and so on. The cells shall be of the type the parameter says: int bool err, dec(N) and str(N) with the same N, or a record. The capacity is not part of the type; any capacity is taken, and l capacity gives the one it has.
09-coll-param.pnx
use io
list scores int 8
list bonus int 8
queue jobs int 4
array grid int 3
word add_range (l list int, lo int, hi int) rem lo, lo + 1, ... up to hi - 1
lo hi range do (v int) v l push or_else (e) end end each
end
word summary (l list int gives str) cat (l len, " of ", l capacity, ", sum ", l sum) end
word fill (q queue int, n int) 0 n range do (v int) v q put or_else (e) end end each end
word zero (a array int) a do (v int, i int) 0 i set a end each_index end
word demo ()
scores 1 4 add_range bonus 10 12 add_range
scores summary io.println
bonus summary io.println
jobs 3 fill cat ("jobs: ", jobs len) io.println
7 1 set grid grid zero cat ("grid: ", 1 grid idx) io.println
end
demo
Output
$ pnx 09-coll-param.pnx
3 of 8, sum 6
2 of 8, sum 21
jobs: 3
grid: 0
(2) In a word with a parameter l that is a table of records, the name l is also the type of its rows, as the name of a declared table is (8.8). A row reached through l is used in that word: in its locals, its fields and the blocks of its loops.
09-coll-param-rows.pnx
use io
record city
name str(12)
people i32
end
list north city 4
list south city 4
word add_city (l list city, s str, n int or fail)
l add_row or_fail to r rem r is a row of l
s r to city.name n i32 r to city.people
end
word largest (l list city gives str)
"" to best -1 to most
l do (r l) r city.people raw most gt if r city.people raw to most r city.name to best end end each
best
end
word demo (or fail)
north "seoul" 9 add_city or_fail north "incheon" 3 add_city or_fail
south "busan" 3 add_city or_fail
cat (north largest, " ", south largest) io.println
end
demo or_else (e) end
Output
$ pnx 09-coll-param-rows.pnx
seoul busan
(2a) map_into and filter_into (10.6) fill a list or an array a parameter names, and copy rows from a table a parameter names.
09-coll-param-fill.pnx
use io
record city
name str(12)
people i32
end
list cities city 4
row cities ("seoul", 9)
row cities ("busan", 3)
row cities ("ulsan", 1)
list big city 4
list counts int 4
list high int 4
word above (l list int, n int, out list int or fail)
l do (x int gives bool) x n gt end out filter_into or_fail
end
word large (l list city, n int or fail)
l do (r l gives bool) r city.people raw n gt end big filter_into or_fail rem rows of l copied
end
word demo (or fail)
cities do (r cities gives int) r city.people raw end counts map_into or_fail
counts 2 high above or_fail
cities 2 large or_fail
cat (high len, " ", big len, " ", 0 big idx city.name) io.println
end
demo or_else (e) e err_text io.println end
Output
$ pnx 09-coll-param-fill.pnx
2 2 seoul
(3) What is passed is the collection itself, not a copy. Collections are globals, so a parameter may be held across yield and the waits of 14.3.
Constraints
(4) A collection shall be given where its kind and cells are those of the parameter.
09-coll-param-kind.pnx
list names str(8) 4
word total (l list int gives int) l sum end
word f (gives int) names total end
Refused
$ pnx 09-coll-param-kind.pnx
09-coll-param-kind.pnx:3:26: the collection given here is of another kind, or holds another type, than the parameter says
(5) A collection parameter is not a value: it is not stored with to, not a result of a word, and not a parameter of a block (word (…)). Pools and indexes are not taken.
09-coll-param-result.pnx
word pick (a list int, b list int gives list int) a end
Refused
$ pnx 09-coll-param-result.pnx
09-coll-param-result.pnx:1:41: a word gives no collection; it takes one as a parameter
(6) A word that truncates a list parameter (8.5 (2a)), or gives it to a word that does, truncates the list given to it. The rule of 8.5 (2b) then holds for that list, where the word is called.
09-coll-param-truncate.pnx
record city
name str(12)
end
list cities city 8
global capital cities rem keeps a row of cities
word reset (l list city) 0 l truncate end
word f () cities reset end
Refused
$ pnx 09-coll-param-truncate.pnx
09-coll-param-truncate.pnx:7:18: this list's rows are kept by a field, a global, a collection, a named row or an index; the word it is given to truncates it
(7) filter_into does not copy rows when both tables are parameters. Walk one with each and fill the other with add_row.
09-coll-param-copy.pnx
record city
name str(12)
end
word keep (src list city, dst list city or fail)
src do (r src gives bool) true end dst filter_into or_fail rem both tables are parameters
end
Refused
$ pnx 09-coll-param-copy.pnx
09-coll-param-copy.pnx:5:7: filter_into does not copy rows between two tables parameters name; walk one and add_row
Diagnostics
| Condition | Message |
|---|---|
| Named and bare inputs mixed | inputs are all named or all bare types, not a mix |
| The stack at the end unlike the results | the stack does not hold exactly the results of the signature here |
| Too few values on the stack | the stack does not hold enough values for this word |
| Values left at the end of the source | values are left on the stack at the end of the source |
| A name that is not defined | this name is not declared before this place |
| A local given another type | this local variable has another type than the value stored into it |
to with the name of a global | this name is a global; a global is written with set: value set name |
to MODULE.NAME into a global | a global of a module is written with set: value set module.name |
set with the name of a local | set writes a global; a local is written with to |
set with a name that is not a global | set writes a declared global; there is none of this name |
| Hiding a visible name | this name is already visible here; a new name may not hide it |
| A word of the language as a new name (the word in the message) | NAME is a word of the language; choose another name |
| A signature unlike the forward declaration | the definition has another signature than its forward declaration |
| A collection of another kind or cells | the collection given here is of another kind, or holds another type, than the parameter says |
| A value where a collection goes | this parameter takes an array, list, queue or buffer: give its name |
| A collection as a result | a word gives no collection; it takes one as a parameter |
to into a collection parameter | this is a collection parameter; a cell of it is stored as: value index set name |
| A kept list given to a word that truncates it | this list's rows are kept by a field, a global, a collection, a named row or an index; the word it is given to truncates it |
| Rows copied between two tables parameters name | filter_into does not copy rows between two tables parameters name; walk one and add_row |