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.

WordBefore → afterMeaning
dupa → a aone more of the top
dropa →discard the top
swapa b → b aexchange the top two
overa b → a b aone more of the second, on top
nipa b → bdiscard 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).

TokenStack after itWhat happened
22A value goes on.
32 3A value goes on.
add5Two values come off; their sum goes on.
45 4A value goes on.
mul20Two 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
ConditionMessage
Named and bare inputs mixedinputs are all named or all bare types, not a mix
The stack at the end unlike the resultsthe stack does not hold exactly the results of the signature here
Too few values on the stackthe stack does not hold enough values for this word
Values left at the end of the sourcevalues are left on the stack at the end of the source
A name that is not definedthis name is not declared before this place
A local given another typethis local variable has another type than the value stored into it
to with the name of a globalthis name is a global; a global is written with set: value set name
to MODULE.NAME into a globala global of a module is written with set: value set module.name
set with the name of a localset writes a global; a local is written with to
set with a name that is not a globalset writes a declared global; there is none of this name
Hiding a visible namethis 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 declarationthe definition has another signature than its forward declaration
A collection of another kind or cellsthe collection given here is of another kind, or holds another type, than the parameter says
A value where a collection goesthis parameter takes an array, list, queue or buffer: give its name
A collection as a resulta word gives no collection; it takes one as a parameter
to into a collection parameterthis is a collection parameter; a cell of it is stored as: value index set name
A kept list given to a word that truncates itthis 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 namefilter_into does not copy rows between two tables parameters name; walk one and add_row