16 Standard modules

(1) The standard modules come with the processor. They are opened with use MODULE and written MODULE.WORD (13.1). Words of standard modules cannot reach outside without the host — they use no files, clocks or environment.

(2) Words of standard modules make no new storage. Their results are one of:

16.1 str — strings

(1) The module str works on strings as bytes. Byte positions count from 0. Only the words that work on characters (chars char_count is_utf8) read UTF-8. Changing case (upper lower) changes ASCII only.

(2) Where a separator or a part to look for is taken (sep, part), a str or a single character (int, 6.5) may be given.

16.1.1 Looking and cutting

(3) All results are views of the string given.

WordSignatureMeaning
len is_empty(str gives int) (str gives bool)the number of bytes; is it empty
take skip(s str, n int gives str)the first n bytes; what follows them; when short, what there is
take_last skip_last(s str, n int gives str)the last n bytes; what comes before them
slice(s str, start int, n int gives str)n bytes from start; out of range is the fault range
trim trim_left trim_right(str gives str)the view without white space (space, tab, CR, LF) at the ends
cut(s str, sep gives str, str)before and after the first sep; without sep, s and the empty text
try_cut(s str, sep gives str, str or fail)like cut, but err.not_found without sep
span(s str, class int gives str, str)the leading bytes of that class, and the rest

(4) The classes of span are the constants str.digit str.alpha str.alnum str.space str.upper_case str.lower_case str.hex_digit str.punct (ASCII).

16-str-views.pnx

use io
use str
word show (label str, s str) cat (label, " [", s, "]") io.println end
word demo ()
  "  key = value  " to line
  "trim" line str.trim show
  "trim_left" line str.trim_left show
  "take 5" "hello, world" 5 str.take show
  "skip 7" "hello, world" 7 str.skip show
  "take_last 5" "hello, world" 5 str.take_last show
  "skip_last 7" "hello, world" 7 str.skip_last show
  "take 99" "short" 99 str.take show                 rem n beyond the end: what is there
  "slice 7 3" "hello, world" 7 3 str.slice show
  "key = value" " = " str.cut to (k, v)
  "cut, before" k show
  "cut, after" v show
  "a=b=c" '=' str.cut to (k2, v2)                    rem the separator may be a character
  "cut at a char" v2 show
  "2026-10-15" str.digit str.span to (year, rest)
  "span digits" year show
  "span rest" rest show
end
demo

Output

$ pnx 16-str-views.pnx
trim [key = value]
trim_left [key = value  ]
take 5 [hello]
skip 7 [world]
take_last 5 [world]
skip_last 7 [hello]
take 99 [short]
slice 7 3 [wor]
cut, before [key]
cut, after [value]
cut at a char [b=c]
span digits [2026]
span rest [-10-15]

16.1.2 Searching and comparing

(5)

WordSignatureMeaning
starts_with ends_with contains(s str, part gives bool)
find find_last(s str, part gives int or fail)the first, last position; err.not_found when absent
compare(str, str gives int)negative, zero or positive, comparing in byte order
byte_at(s str, i int gives u8)byte i; out of range is the fault range
char_count(str gives int)the number of UTF-8 characters
is_utf8(str gives bool)is it valid UTF-8

The comparison words eq ne lt and the rest also compare strings in byte order (15.2).

16-str-search.pnx

use io
use str
word yes (b bool gives str) b if "yes" else "no" end end
"hello.pnx" ".pnx" str.ends_with yes io.println
"hello.pnx" "he" str.starts_with yes io.println
"hello.pnx" "lo." str.contains yes io.println
"a,b,c,b" ",b" str.find or_else (e) -1 end str.to_str io.println
"a,b,c,b" ",b" str.find_last or_else (e) -1 end str.to_str io.println
"a,b,c,b" "x" str.find or_else (e) -1 end str.to_str io.println
"abc" "abd" str.compare str.to_str io.println
"hello" 1 str.byte_at str.to_str io.println
"hello" str.len str.to_str io.println
"" str.is_empty yes io.println

Output

$ pnx 16-str-search.pnx
yes
yes
yes
1
5
-1
-1
101
5
yes

16.1.3 Making new text

(6) The results are new bytes written in the region; when the region is used up it is the fault full (12.2). The words that read numbers can fail.

WordSignatureMeaning
upper lower(str gives str)ASCII case changed
repeat(s str, n int gives str)n copies joined; a negative n is the fault range
replace(s str, old str, new str gives str)every old replaced by new
pad_left pad_right(s str, width int gives str)filled with spaces to width bytes; as it is when longer; a width below 0 or from 232 up is the fault range
to_str(int gives str)decimal text; a dec(N) with its N digits after the point (7.5)
to_dec(s str, N gives dec(N) or fail)read a decimal: a sign, digits, and a point with at most N digits after it (N a constant); more digits, or anything else, err.format
to_int(str gives int or fail)read as decimal: one sign and digits only (no spaces); otherwise, or beyond 64 bits, err.format
to_int_base(s str, base int gives int or fail)read in base 2–36; another base is the fault range

16-str-make.pnx

use io
use str
"Hello, World" str.upper io.println
"Hello, World" str.lower io.println
"ab" 3 str.repeat io.println
"one two one" "one" "1" str.replace io.println
cat ("[", "7" 4 str.pad_left, "]") io.println
cat ("[", "7" 4 str.pad_right, "]") io.println
-42 str.to_str io.println
"ff" 16 str.to_int_base or_else (e) -1 end str.to_str io.println
"  12" str.to_int or_else (e) -1 end str.to_str io.println
"12x" str.to_int or_else (e) -1 end str.to_str io.println
"한글" str.len str.to_str io.println
"한글" str.char_count str.to_str io.println
"\xff" str.is_utf8 if "utf-8" else "not utf-8" end io.println

Output

$ pnx 16-str-make.pnx
HELLO, WORLD
hello, world
ababab
1 two 1
[   7]
[7   ]
-42
255
-1
-1
6
2
not utf-8

16.1.4 Stems and writing into buffers

(7) Words that make stems (10.6):

(8) Words that write into a buffer:

16-str-stems.pnx

use io
use str
"one two  three" str.words do (w str) cat ("<", w, ">") io.print end each
"" io.println
"a,b,,c" ',' str.split do (w str) cat ("<", w, ">") io.print end each
"" io.println
text END
first line
second line
END
str.lines do (l str) cat ("line: ", l) io.println end each
"hi" str.bytes do (b u8) b str.to_str io.print " " io.print end each
"" io.println
"가a!" str.chars do (c str) cat ("<", c, " ", c str.len, ">") io.print end each
"" io.println
buffer out 64
", " out "x y z" str.words str.join_into or_else (e) end
out contents io.println
out clear
word swap_dashes () "a-b-c" "-" "+" out str.replace_into or_else (e) 0 end to n  cat (out contents, " (", n, " replaced)") io.println end
swap_dashes

Output

$ pnx 16-str-stems.pnx
<one><two><three>
<a><b><><c>
line: first line
line: second line
104 105 
<가 3><a 1><! 1>
x, y, z
a+b+c (2 replaced)
Constraints

(9) A value that is not a str given to a word of str is refused.

16-str-type.pnx

use str
42 str.upper drop

Refused

$ pnx 16-str-type.pnx
16-str-type.pnx:2:4: this word takes a str
Faults

(10) An out-of-range position for slice or byte_at is a fault. For cuts that may run short, use take and skip.

16-str-slice-range.pnx

use io
use str
"hello" 3 5 str.slice io.println

Fault

$ pnx 16-str-slice-range.pnx
16-str-slice-range.pnx:3: index out of range

16-str-byte-range.pnx

use str
"hello" 5 str.byte_at drop

Fault

$ pnx 16-str-byte-range.pnx
16-str-byte-range.pnx:2: index out of range

16.2 fmt — numbers as text

(1) cat puts an int in decimal (9.8). Other forms are made into a str by the words of the module fmt. There is no small language of formats (such as %08x). Results are written in the region.

WordSignatureMeaning
hex bin(n gives str)hexadecimal (lower case), binary; n an int or a narrow integer (7.6): a negative int as 64-bit two’s complement, a narrow integer as the bits of its width
hex_pad(n, width int gives str)hexadecimal filled with leading zeros to width digits; n as for hex
zero_pad(n, width int gives str)decimal filled with leading zeros to width; the sign takes a place; n an int or a narrow integer (a u64 unsigned)
append_hex(n, buf or fail)appends the hexadecimal to a buffer; n as for hex

Decimal is str.to_str; decimal appended to a buffer is append_int (15.3). A width of hex_pad or zero_pad below 0 or from 232 up is the fault range; when the region is used up it is the fault full.

16-fmt.pnx

use io
use fmt
255 fmt.hex io.println
5 fmt.bin io.println
255 8 fmt.hex_pad io.println
7 3 fmt.zero_pad io.println
-7 3 fmt.zero_pad io.println
buffer out 32
"0x" out append or_else (e) end
48879 out fmt.append_hex or_else (e) end
out contents io.println

Output

$ pnx 16-fmt.pnx
ff
101
000000ff
007
-07
0xbeef
Constraints

(2) The words of fmt take an int. A value of another type is refused. A dec(N) is written by str.to_str and cat (7.5).

16-fmt-type.pnx

use fmt
use io
"abc" fmt.hex io.println

Refused

$ pnx 16-fmt-type.pnx
16-fmt-type.pnx:3:7: this word takes an int or a narrow integer
Faults

(3) A width of hex_pad or zero_pad below 0 or from 232 up is the fault range.

16-fmt-width.pnx

use fmt
use io
255 -1 fmt.hex_pad io.println

Fault

$ pnx 16-fmt-width.pnx
16-fmt-width.pnx:3: index out of range

(4) A result that does not fit in the region is the fault full. The width below is allowed, but larger than the region.

16-fmt-full.pnx

use fmt
use str
use io
7 4294967295 fmt.zero_pad str.len str.to_str io.println

Fault

$ pnx 16-fmt-full.pnx
16-fmt-full.pnx:4: a declared capacity is used up
Diagnostics
ConditionMessage
a value that is not an intthis word takes an int

16.3 hash — hashes

(1) The module hash gives hashes of bytes as an int. There are no cryptographic hashes; a host gives those as host words.

WordSignatureMeaning
crc32(str gives u32)CRC-32 (IEEE 802.3, as zlib)
fnv1a(str gives u64)64-bit FNV-1a
hash64(str gives u64)SipHash-2-4 with the key 0

16-hash.pnx

use io
use hash
use fmt
"hello" hash.crc32 fmt.hex io.println
"hello" hash.fnv1a fmt.hex io.println
"hello" hash.hash64 fmt.hex io.println

Output

$ pnx 16-hash.pnx
3610a686
a430d84680aabd0b
8cc15d5db2f752b9

(1a) Check values: the crc32 of the empty string is 0, and that of "123456789" is cbf43926 (the standard check value of CRC-32). The fnv1a of the empty string is FNV’s offset basis cbf29ce484222325. Another processor shall give the same values.

16-hash-vectors.pnx

use io
use str
use fmt
use hash
"" hash.crc32 str.to_str io.println
"123456789" hash.crc32 fmt.hex io.println
"" hash.fnv1a fmt.hex io.println
"" hash.hash64 fmt.hex io.println

Output

$ pnx 16-hash-vectors.pnx
0
cbf43926
cbf29ce484222325
1e924b9d737700d7
Constraints

(2) The hash of a value that is not a str is refused. To hash a number, make bytes of it first (16.5).

16-hash-type.pnx

use hash
42 hash.crc32 drop

Refused

$ pnx 16-hash-type.pnx
16-hash-type.pnx:2:4: this word takes a str

16.4 encode — bytes as text

(1) The words of the module encode are all (s str, buf or fail). They append the result to the buffer and fail with err.full when there is not enough room, or err.format when the input has the wrong form.

WordMeaning
base64_encode base64_decodeBase64 of RFC 4648; when decoding, the closing = may be left out
hex_encode hex_decodetwo hexadecimal digits per byte; lower case when writing, either case when reading
url_encode url_decodepercent-encoding of RFC 3986: bytes other than letters, digits and - . _ ~ as %XX

16-encode.pnx

use io
use encode
buffer out 64
word show (label str) cat (label, " ", out contents) io.println  out clear end
word demo (or fail)
  "Pinax!" out encode.base64_encode or_fail  "base64" show
  "UGluYXgh" out encode.base64_decode or_fail  "decoded" show
  "\x00\xff" out encode.hex_encode or_fail  "hex" show
  "a b&c=d/é" out encode.url_encode or_fail  "url" show
  "a%20b" out encode.url_decode or_fail  "url decoded" show
end
demo or_else (e) cat ("failed: ", e err_text) io.println end

Output

$ pnx 16-encode.pnx
base64 UGluYXgh
decoded Pinax!
hex 00ff
url a%20b%26c%3Dd%2F%C3%A9
url decoded a b

16-encode-bad.pnx

use io
use encode
buffer out 16
"zz" out encode.hex_decode or_else (e) cat ("hex_decode: ", e err_text) io.println end
"ab*c" out encode.base64_decode or_else (e) cat ("base64_decode: ", e err_text) io.println end

Output

$ pnx 16-encode-bad.pnx
hex_decode: format
base64_decode: format
Constraints

(2) A value that is not a str and a buffer is refused. To encode a number, make text or bytes of it first (16.1, 16.5).

16-encode-type.pnx

use encode
buffer out 8
5 out encode.hex_encode or_else (e) end

Refused

$ pnx 16-encode-type.pnx
16-encode-type.pnx:3:7: this word takes a str and a buffer, as: s buf encode.base64_encode
Diagnostics
ConditionMessage
a value that is not a str and a bufferthis word takes a str and a buffer, as: s buf encode.base64_encode

16.5 bytes — binary data

(1) The module bytes writes numbers and text into a buffer as fixed byte forms, and reads them from bytes. The writing words are (value, buf or fail) (err.full when there is not enough room; nothing is written). The reading words are (s str gives value, rest str or fail) (err.format when the bytes run short or have the wrong form).

(2)

WordBytes
put_u8 put_u16 put_u32 put_u64
put_i8 put_i16 put_i32 put_i64
a value of the type the name says (7.6; put_i64 an int), least significant byte first
the same names with _bemost significant byte first
get_u8 … get_i64, get_u16_be …their pairs: a value of the type the name says (get_i64 an int)
put_varint get_varintLEB128: the value as an unsigned 64-bit number, 7 bits at a time (a negative takes 10 bytes)
put_str get_stra length (varint), then the bytes

16-bytes.pnx

use io
use bytes
use encode
use str
buffer out 64
buffer hex 128
word dump () out contents hex encode.hex_encode or_else (e) end  hex contents io.println  hex clear end
word demo (or fail)
  258 u16 out bytes.put_u16 or_fail            rem little end first: 02 01
  258 u16 out bytes.put_u16_be or_fail         rem big end first: 01 02
  300 out bytes.put_varint or_fail         rem LEB128: ac 02
  "hi" out bytes.put_str or_fail           rem a varint length, then the bytes
  dump
  out contents to s
  s bytes.get_u16 or_fail to (a, s1)       rem the value and the rest
  s1 bytes.get_u16_be or_fail to (b, s2)
  s2 bytes.get_varint or_fail to (c, s3)
  s3 bytes.get_str or_fail to (d, s4)
  cat (a, " ", b, " ", c, " ", d, ", left ", s4 str.len) io.println
end
demo or_else (e) e err_text io.println end

Output

$ pnx 16-bytes.pnx
02010102ac02026869
258 258 300 hi, left 0

16.5.1 Records

(3) row buf bytes.pack appends one row of a record (8.8) to a buffer, field by field in order (or fail). s row bytes.unpack reads the fields from bytes into the row and gives the rest (gives str or fail). row bytes.packed_size is the number of bytes pack would write.

(4) The bytes of a field: an integer width as that width (most significant byte first when the record has big_endian, least significant first otherwise), int as 8 bytes, bool as 1 byte, varint as LEB128, str(N) as a length (varint) and the bytes, a field holding a row as the number it holds (varint).

(5) When pack fails the buffer goes back to the length it had before the call. unpack writes into the row only when every field can be read; when it fails the row is unchanged.

16-bytes-pack.pnx

use io
use bytes
use encode
use str
record item
  id u16
  name str(8)
  delta varint
end
record wire
  big_endian
  id u16
  count u32
end
array items item 2
array wires wire 1
buffer out 64
buffer hex 128
word dump () out contents hex encode.hex_encode or_else (e) end  hex contents io.println  hex clear end
word demo (or fail)
  0 items idx to a
  513 u16 a to item.id  "pinax" a to item.name  -3 a to item.delta
  a out bytes.pack or_fail
  dump
  cat ("packed_size ", a bytes.packed_size, ", written ", out contents str.len) io.println
  1 items idx to b
  out contents b bytes.unpack or_fail to rest
  cat (b item.id, " ", b item.name, " ", b item.delta, ", rest ", rest str.len) io.println
  "\x01\x02\x05" b bytes.unpack or_else (e) cat ("short input: ", e err_text) io.println "" end drop
  cat ("the row is unchanged: ", b item.name) io.println
  out clear
  0 wires idx to w
  258 u16 w to wire.id  7 u32 w to wire.count
  w out bytes.pack or_fail
  dump
end
demo or_else (e) e err_text io.println end

Output

$ pnx 16-bytes-pack.pnx
01020570696e6178fdffffffffffffffff01
packed_size 18, written 18
513 pinax -3, rest 0
short input: format
the row is unchanged: pinax
010200000007
Constraints

(6) pack, unpack and packed_size take only rows of records.

16-bytes-pack-int.pnx

use bytes
buffer b 8
5 b bytes.pack or_else (e) end

Refused

$ pnx 16-bytes-pack-int.pnx
16-bytes-pack-int.pnx:3:5: pack takes a row of a table of records and a buffer: row buf bytes.pack
Faults

(7) The value written has the type the word’s name says (u8 for put_u8, 7.6). An int is made into that type first (n u8), and a value outside its range is the fault overflow there; nothing is cut.

16-bytes-narrow.pnx

use bytes
buffer b 8
word store (n int) n u8 b bytes.put_u8 or_else (e) end end
300 store

Fault

$ pnx 16-bytes-narrow.pnx
16-bytes-narrow.pnx:3: integer overflow
  n int = 300

16.6 ptn — small patterns

(1) The module ptn finds and replaces text with patterns of the size of Lua’s. It has no alternation (|), no repetition of groups and no back references; rex (16.7) takes the larger grammar.

16.6.1 The text of a pattern

(2) A pattern is a sequence of the items below, matched byte by byte.

ItemMatches
an ordinary characterthat byte
.any byte
%a %d %l %u %s %w %x %p %cletters, digits, lower case, upper case, white space, letters or digits, hexadecimal digits, punctuation, control characters (ASCII); in upper case, everything else
% and a character that is not a letter or digitthat byte itself (%. %% %()
[…] [^…]a set and its complement, with a-z ranges and % classes inside; a ] right after [ or [^ is a member
* + - ? after an itemzero or more (longest), one or more (longest), zero or more (shortest), zero or one
( … )a capture; up to nine
^ first, $ lastanchored at the start, at the end of the text

16-ptn-classes.pnx

use io
use ptn
"a1 B2_c3!" "%a" "." ptn.replace io.println     rem letters
"a1 B2_c3!" "%d" "." ptn.replace io.println     rem digits
"a1 B2_c3!" "%W" "." ptn.replace io.println     rem not letter or digit
"a1 B2_c3!" "[%u_]" "." ptn.replace io.println  rem a set: upper case or _
"aaab" "a-b" "<%0>" ptn.replace io.println      rem - is the shortest repetition
"color colour" "colou?r" "#" ptn.replace io.println

Output

$ pnx 16-ptn-classes.pnx
.1 .2_.3!
a. B._c.!
a1.B2.c3.
a1 .2.c3!
<aaab>
# #

16.6.2 Words

(3)

WordSignatureMeaning
is_match(s str, pat gives bool)is there a match anywhere
match(s str, pat gives str or fail)a view of the first match; err.not_found when none
capture(n int gives str)capture n (1–9) of the last match; 0 is the whole match
matchesstemthe matches in order, each search starting where the last ended
replace(s str, pat, rep str gives str)new text with every match replaced by rep; in rep, %0–%9 are captures and %% is %

(4) A pattern that begins with ^ is tried at the start only, so matches and replace match at most once.

(5) The last match capture reads belongs to each run (task) separately; another match changes it. A run the host starts begins with no match: the match of an earlier run is not seen, since its captures are views of text that run matched, which may be gone.

16-ptn.pnx

use io
use ptn
const date "(%d+)-(%d+)-(%d+)"
word yes (b bool gives str) b if "true" else "false" end end
"abc 123" "%d+" ptn.is_match yes io.println
word demo ()
  "on 2026-10-15 ok" date ptn.match or_else (e) "?" end io.println    rem the whole match
  cat (1 ptn.capture, "/", 2 ptn.capture, "/", 3 ptn.capture) io.println
  "key = value" "^(%w+)%s*=%s*(%w+)$" ptn.match or_else (e) "?" end drop
  cat (2 ptn.capture, " <- ", 1 ptn.capture) io.println
end
demo
"x=1, y=22, z=333" "%a=%d+" ptn.matches do (m str) cat ("[", m, "]") io.print end each
"" io.println
"2026-10-15" date "%3.%2.%1 (%0)" ptn.replace io.println
"  trim me  " "^%s*(.-)%s*$" "%1" ptn.replace io.println
"aaa" "^a" "-" ptn.replace io.println              rem ^ anchors: one match, at the start

Output

$ pnx 16-ptn.pnx
true
2026-10-15
2026/10/15
value <- key
[x=1][y=22][z=333]
15.10.2026 (2026-10-15)
trim me
-aa
Constraints

(6) A pattern shall be a string literal or a const of one, so that it is checked at load. To search with text made while running, use rex.compile (16.7).

16-ptn-variable.pnx

use ptn
word f (s str, p str gives bool) s p ptn.is_match end

Refused

$ pnx 16-ptn-variable.pnx
16-ptn-variable.pnx:2:38: a pattern is a string literal or a const name, so that it is checked at load

(7) A malformed pattern (an unclosed set or capture, a final %, a ) that closes nothing, ten or more captures) is refused; so are %b, %f, the back reference %1 and the position capture ().

16-ptn-bad.pnx

use ptn
word f (s str gives bool) s "[a-z" ptn.is_match end

Refused

$ pnx 16-ptn-bad.pnx
16-ptn-bad.pnx:2:36: a set is not closed by ]

16-ptn-balance.pnx

use ptn
word f (s str gives bool) s "%b()" ptn.is_match end

Refused

$ pnx 16-ptn-balance.pnx
16-ptn-balance.pnx:2:36: %b, %f and back references are not supported
Faults

(8) A capture whose n the last match did not make, or with no match yet, is the fault range.

16-ptn-capture-none.pnx

use ptn
use io
word f () "ab" "(a)b" ptn.match or_else (e) "" end drop  2 ptn.capture io.println end
f

Fault

$ pnx 16-ptn-capture-none.pnx
16-ptn-capture-none.pnx:3: index out of range

(9) Matching goes back and tries again. A word that takes more than 224 steps in one call stops with the fault depth. Rewrite such a pattern with fewer shortest repetitions (-), or use rex, which does not go back.

16-ptn-steps.pnx

use ptn
use str
"a" 300 str.repeat ".-.-.-.-.-.-.-.-x" ptn.is_match drop

Fault

$ pnx 16-ptn-steps.pnx
16-ptn-steps.pnx:3: call depth limit
Diagnostics
ConditionMessage
A pattern that is not a constanta pattern is a string literal or a const name, so that it is checked at load
An unclosed seta set is not closed by ]
%b, %f, back references%b, %f and back references are not supported

16.7 rex — regular expressions

(1) The module rex finds and replaces with regular expressions. Its matching never goes back, so the time it takes is bounded by the length of the text times the size of the pattern. Write patterns as raw strings (6.5): in an ordinary string "\d" is refused as an unknown escape, and "\b" becomes a backspace.

(2) In a source that has use rex the type name regex is visible. A regex value is a pattern program that cannot be changed once made. It is used only in locals, arguments and results.

16.7.1 The text of a pattern

(3)

FormMeaning
a characterits bytes; a multi-byte character, its byte sequence
.one UTF-8 character; without (?s) not a line break
[abc] [a-z] [^…]a set; ranges are ASCII; a complemented set takes one UTF-8 character
\d \w \s \D \W \Sdigits, word characters, white space and their complements (ASCII)
* + ? {n} {n,} {n,m}repetition (longest); with ? after it, shortest
a|balternation; the left first
( … ) (?: … )a group that captures, one that does not
^ $the start and end of the text; with (?m), of each line
\b \Ba word boundary and its complement (ASCII)
\n \t \r \f \v \0 \xNNthat byte
\ and any other characterthat character (\. \* \\)
\zs \zethe match is narrowed to start, end here; the whole pattern must still match
(?i) (?m) (?s)at the very start of a pattern only: ignore case (ASCII), anchors per line, . also matches a line break

There are no back references, lookaround, Unicode properties or named groups.

(4) A pattern that repeats a body able to match the empty text (such as (a*)*) may give other captures than Perl or Python.

16.7.2 Words

(5) re is a regex value. “The match” is the range narrowed by \zs and \ze.

WordSignatureMeaning
rx(pat gives regex)makes the program at load; pat is a literal or a const
compile(pat str gives regex or fail)makes it while running; a malformed pattern is err.format; the program lies in the region
is_match(s str, re gives bool)is there a match
match(s str, re gives str or fail)the first match; err.not_found when none
cut_match(s str, re gives str, str, str or fail)what is before the first match, the match, and what is after
matchesstemthe matches in order; the block takes the whole match and one str parameter per group
splitstemthe pieces between the matches
with_match(s str, re, block or fail)runs the block once with the first match; err.not_found when none
replace(s str, re, rep str gives str)new text with every match replaced; in rep, \0–\9 are groups
replace_into(s str, re, rep str, buf gives int or fail)appends the replaced text to a buffer and gives the number of replacements
replace_by(s str, re, buf, block gives int or fail)appends to a buffer, each match replaced by the text the block gives

(6) When matches, split and replace search again from the next place, they still see the whole text, so ^ and \b know the bytes before that place.

16-rex.pnx

use io
use str
use rex
const pair r"(\w+)=(\d+)"
word yes (b bool gives str) b if "true" else "false" end end
word total (line str gives n int)               rem groups arrive as block parameters
  0 to n
  line pair rex.rx rex.matches do (whole str, key str, value str)
    n value str.to_int or_else 0 end add to n
  end each
  n
end
word demo ()
  cat ("sum ", "id=7, x=20; name=kim, n=3" total) io.println
  "abc 123" r"\d+" rex.rx rex.is_match yes io.println
  "on 2026-10-15 ok" r"(\d+)-(\d+)" rex.rx rex.match or_else (e) "?" end io.println
  "key: value" r":\s*" rex.rx rex.cut_match or_else (e) "" "" "" end to (before, m, after)
  cat ("[", before, "|", m, "|", after, "]") io.println
  "from 2026-10-13 to 2026-11-02" r"(\d{4})-(\d{2})-(\d{2})" rex.rx r"\3/\2/\1" rex.replace io.println
  "a , b;c ,d" r"\s*[,;]\s*" rex.rx rex.split do (part str) cat ("<", part, ">") io.print end each
  "" io.println
  "가나다" r"." rex.rx rex.matches do (ch str) cat ("<", ch, ">") io.print end each   rem . is one UTF-8 character
  "" io.println
  "foobar" r"foo\zsbar" rex.rx rex.match or_else (e) "?" end io.println              rem \zs: the match starts here
  "Hello" r"(?i)HELLO" rex.rx rex.is_match yes io.println
end
demo

Output

$ pnx 16-rex.pnx
sum 30
true
2026-10
[key|: |value]
from 13/10/2026 to 02/11/2026
<a><b><c><d>
<가><나><다>
bar
true

16-rex-blocks.pnx

use io
use str
use rex
buffer out 64
word demo ()
  "2026-10-15" r"(\d+)-(\d+)-(\d+)" rex.rx do (whole str, y str, m str, d str)
    cat (d, "/", m, "/", y) io.println
  end rex.with_match or_else (e) "no date" io.println end
  "none here" r"\d+" rex.rx do (whole str) whole io.println end rex.with_match or_else (e) "no date" io.println end
  "the quick brown fox" r"\b[a-z]" rex.rx out do (w str gives str) w str.upper end rex.replace_by or_else (e) -1 end to n
  cat (n, " replaced: ", out contents) io.println
end
demo

Output

$ pnx 16-rex-blocks.pnx
15/10/2026
no date
4 replaced: The Quick Brown Fox

16-rex-compile.pnx

use io
use rex
buffer out 64
word swap_with (p str, s str gives n int or fail)  rem a pattern made while running
  out clear
  p rex.compile or_fail to re
  s re "-" out rex.replace_into or_fail
end
word demo ()
  "X" "aXbXc" swap_with or_else (e) -1 end to n
  cat (n, " replaced: ", out contents) io.println
  "(" "aXbXc" swap_with or_else (e) cat ("bad pattern: ", e err_text) io.println -1 end drop
end
demo

Output

$ pnx 16-rex-compile.pnx
2 replaced: a-b-c
bad pattern: format
Constraints

(7) The pattern of rx shall be a string literal or a const. A pattern made while running goes through compile.

16-rex-variable.pnx

use rex
word f (s str, p str gives bool) s p rex.rx rex.is_match end

Refused

$ pnx 16-rex-variable.pnx
16-rex-variable.pnx:2:38: rx takes a string literal or a const name; a pattern made while running is rex.compile

(8) Malformed patterns and missing features (back references and the like) are refused.

16-rex-bad.pnx

use rex
word f (s str gives bool) s r"a{3,1}" rex.rx rex.is_match end

Refused

$ pnx 16-rex-bad.pnx
16-rex-bad.pnx:2:39: a counted repetition goes downwards

16-rex-backref.pnx

use rex
word f (s str gives bool) s r"(a)\1" rex.rx rex.is_match end

Refused

$ pnx 16-rex-backref.pnx
16-rex-backref.pnx:2:38: back references are not supported

(9) A str where a regex goes is refused. The group parameters of a block shall be str.

16-rex-str.pnx

use rex
word f (s str gives bool) s "a" rex.is_match end

Refused

$ pnx 16-rex-str.pnx
16-rex-str.pnx:2:33: this word takes a str and a regex: s re rex.match

16-rex-group-type.pnx

use rex
word f (s str) s r"(a)" rex.rx rex.matches do (m str, g int) end each end

Refused

$ pnx 16-rex-group-type.pnx
16-rex-group-type.pnx:2:44: the groups are named str parameters
Faults

(10) The work space of a search is borrowed from the end of the region on every call. A region too small is the fault full. The search below comes after the region was nearly filled.

16-rex-full.pnx

use rex
use str
use io
word found (gives bool)
  "x" 4194000 str.repeat to s
  s r"(a|b|c|d|e|f|g|h)+x" rex.rx rex.is_match
end
found if "yes" else "no" end io.println

Fault

$ pnx 16-rex-full.pnx
16-rex-full.pnx:6: a declared capacity is used up
  s str = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
Diagnostics
ConditionMessage
A pattern for rx that is not a constantrx takes a string literal or a const name; a pattern made while running is rex.compile
A count that goes downwardsa counted repetition goes downwards
A back referenceback references are not supported
A str where a regex goesthis word takes a str and a regex: s re rex.match
A group parameter that is not a strthe groups are named str parameters

16.8 json — JSON

(1) The module json reads and writes JSON (RFC 8259) text in one direction, without building a tree: it reads one token at a time and writes straight into a buffer.

16.8.1 Reading

(2) s json.next reads the first token of s and gives (kind int, value str, rest str) (or fail). At the end it fails with err.not_found; on something that is not a token, with err.format. White space, commas and colons between tokens are passed over. The structure of the grammar — matching brackets, where commas go — is not checked.

(3) kind is one of the constants below. The value of a string is its text without the quotes; when it has escapes, they are decoded into text written in the region. The value of a number is its text as written.

ConstantToken
json.object json.object_end{ }
json.array json.array_end[ ]
json.keya string followed by a colon
json.stringany other string
json.numbera number
json.boolean json.nulltrue false, null

(4) s json.skip passes over one value (with everything inside, for an object or array) and gives the rest (gives str or fail).

16-json-read.pnx

use io
use json
word kind_name (k int gives str)                    rem the kinds are constants of json
  k json.key eq if "key" return end
  k json.string eq if "string" return end
  k json.number eq if "number" return end
  k json.boolean eq if "boolean" return end
  k json.null eq if "null" return end
  k json.object eq k json.array eq or if "open" return end
  "close"
end
word tokens (s str)
  s to rest
  loop
    rest json.next or_else (e) break end to (kind, value, rest)
    cat (kind kind_name, " ", value) io.println
  end
end
text END
{"name": "kim", "tags": [1, -2.5e3, true, null], "note": "café"}
END
tokens

Output

$ pnx 16-json-read.pnx
open {
key name
string kim
key tags
open [
number 1
number -2.5e3
boolean true
null null
close ]
key note
string café
close }

16.8.2 Writing

(5) The writing words all append to a buffer and fail with err.full when there is not enough room. They put the commas in themselves.

WordSignature
begin_object end_object begin_array end_array put_null(buf or fail)
put_key put_str(s str, buf or fail); the word does the escaping
put_int(n int, buf or fail)
put_dec(d dec(N), buf or fail)
put_bool(b bool, buf or fail)

16-json-write.pnx

use io
use json
buffer out 128
word demo (or fail)
  out json.begin_object or_fail
  "name" out json.put_key or_fail  "kim \"k\"" out json.put_str or_fail
  "tags" out json.put_key or_fail
  out json.begin_array or_fail  1 out json.put_int or_fail  true out json.put_bool or_fail  out json.put_null or_fail
  out json.end_array or_fail
  out json.end_object or_fail
end
demo or_else (e) e err_text io.println end
out contents io.println

Output

$ pnx 16-json-write.pnx
{"name":"kim \"k\"","tags":[1,true,null]}

16.8.3 Records

(6) row buf json.pack appends a row of a record as an object whose keys are the field names. s row
json.unpack
reads an object into the fields of the same names and gives the rest after the object. Values of unknown keys are passed over. Numbers shall be integers that fit the field’s width; a dec(N) field takes a number with at most N digits after the point and is written with all N (7.5); null is not accepted. The row is written only when everything is right; when it fails (err.format) the row is unchanged.

16-json-record.pnx

use io
use json
record user
  name str(8)
  id u16
  admin bool
end
list users user 4
buffer out 128
word input (gives str)
  text END
{"id": 9, "extra": {"x": [1, 2]}, "name": "park", "admin": false} and more
END
end
word demo (or fail)
  users add_row or_fail to u
  "lee" u to user.name  7 u16 u to user.id  true u to user.admin
  u out json.pack or_fail
  out contents io.println
  users add_row or_fail to v
  input v json.unpack or_fail to rest                  rem unknown keys are skipped
  cat (v user.name, " ", v user.id, " ", v user.admin, " /", rest, "/") io.println
  "{\"id\": 70000}" v json.unpack or_else (e) cat ("70000 in u16: ", e err_text) io.println "" end drop
  cat ("the row is unchanged: ", v user.id) io.println
end
demo or_else (e) e err_text io.println end

Output

$ pnx 16-json-record.pnx
{"name":"lee","id":7,"admin":true}
park 9 false / and more/
70000 in u16: format
the row is unchanged: 9

16-dec-text.pnx

use io
use json
use csv
record item
  name str(8)
  cost dec(2)
end
array one item 1
buffer out 64
word demo (or fail)
  0 one idx to r
  "pen" r to item.name  1.5 1 rescale 2 rescale r to item.cost
  r out json.pack or_fail  out contents io.println
  out clear  r out csv.pack or_fail  out contents io.print
  "{\"cost\": 3.05}" r json.unpack or_fail drop  cat (r item.cost) io.println
  "{\"cost\": 3.055}" r json.unpack or_else (e) cat ("3.055: ", e err_text) io.println "" end drop
end
demo or_else (e) e err_text io.println end

Output

$ pnx 16-dec-text.pnx
{"name":"pen","cost":1.50}
pen,1.50
3.05
3.055: format
Constraints

(7) json.next and json.skip take only a str; pack and unpack only rows of records.

16-json-type.pnx

use json
word f () 5 json.next drop drop drop end

Refused

$ pnx 16-json-type.pnx
16-json-type.pnx:2:13: this word takes the JSON text as a str
Diagnostics
ConditionMessage
JSON text that is not a strthis word takes the JSON text as a str

16.9 csv — CSV

(1) The module csv reads and writes CSV in the form of RFC 4180: fields are separated by commas; a field that holds a comma, a quote or a line break is quoted, and quotes inside it are doubled.

(2) s csv.fields is a stem (10.6) of the fields of one line. A quoted field is given without its quotes and with doubled quotes made single. An unclosed quote gives the rest of the text as the last field (a stem cannot report a failure).

(3) The writing words append to a buffer (or fail, err.full when there is not enough room). s buf csv.put, n buf csv.put_int, d buf csv.put_dec and b buf csv.put_bool append one field (with the comma it needs); buf csv.end_line ends the line. Booleans are written true and false.

(4) row buf csv.pack appends one row of a record as one line. s row csv.unpack reads one line into the fields in order and gives what follows the line. The row is written only when every field is right (err.format). A dec(N) field is read and written as in json (16.8).

16-csv.pnx

use io
use csv
record user
  name str(8)
  id u16
  admin bool
end
list users user 4
buffer out 128
"a,\"b,c\",\"d\"\"e\",,f" csv.fields do (x str) cat ("[", x, "]") io.print end each
"" io.println
word demo (or fail)
  users add_row or_fail to u
  "lee, jr" u to user.name  7 u16 u to user.id  true u to user.admin
  u out csv.pack or_fail                               rem a line, quoted where needed
  "x" out csv.put or_fail  5 out csv.put_int or_fail  false out csv.put_bool or_fail  out csv.end_line or_fail
  out contents io.print
  users add_row or_fail to v
  "zed,5,true\nrest" v csv.unpack or_fail to rest
  cat (v user.name, " ", v user.id, " ", v user.admin, " [", rest, "]") io.println
  "zed,5,maybe" v csv.unpack or_else (e) cat ("bad bool: ", e err_text) io.println "" end drop
end
demo or_else (e) e err_text io.println end

Output

$ pnx 16-csv.pnx
[a][b,c][d"e][][f]
"lee, jr",7,true
x,5,false
zed 5 true [rest]
bad bool: format
Constraints

(5) pack and unpack take only rows of records.

16-csv-pack-int.pnx

use csv
buffer b 8
word f () 5 b csv.pack or_else (e) end end

Refused

$ pnx 16-csv-pack-int.pnx
16-csv-pack-int.pnx:3:15: pack takes a row of a table of records and a buffer: row buf csv.pack
Diagnostics
ConditionMessage
A value that is not a row of a recordpack takes a row of a table of records and a buffer: row buf csv.pack