Standard library

1189 functions across 84 modules, all built in

No package manager: what ships with the compiler is what you can call. This page asked the compiler serving it for the list, so it cannot be out of date. Need something that is not here? Copy it in, that is the whole installation, and what everyone keeps reaching for gets added to this list.

archive 9 functions
archive_targz_create(archive_path:s, directory:s):v!e

Writes every file at or below the directory into one gzipped tar file.

archive_targz_extract(archive_path:s, directory:s):v!e

Unpacks a gzipped tar file into a directory. An entry naming a path outside that directory is refused, and links and devices are skipped.

archive_targz_list(archive_path:s):a:s!e

Returns the paths inside a gzipped tar file without unpacking it.

archive_tarzst_create(archive_path:s, directory:s):v!e

Writes every file at or below the directory into one zstd compressed tar file.

archive_tarzst_extract(archive_path:s, directory:s):v!e

Unpacks a zstd compressed tar file into a directory. An entry naming a path outside that directory is refused, and links and devices are skipped.

archive_tarzst_list(archive_path:s):a:s!e

Returns the paths inside a zstd compressed tar file without unpacking it.

archive_zip_create(zip_path:s, directory:s):v!e

Writes every file at or below the directory into one compressed zip file.

archive_zip_extract(zip_path:s, directory:s):v!e

Unpacks a zip file into a directory, creating it if needed. An entry naming a path outside that directory is refused rather than written.

archive_zip_list(zip_path:s):a:s!e

Returns the paths inside a zip file without unpacking it.

args 10 functions
args_count():i

Returns the number of command-line arguments.

args_flag(name:s):b

Returns true if a flag like --verbose is present.

args_get(index:i):s!e

Returns the positional command-line argument at the index. Errors if out of range.

args_help_text(program:s, description:s, options:a:ARGS_Option):s

Builds the --help page from the program's own description of its options, so the page cannot drift from what the program accepts.

args_parse(options:a:ARGS_Option):ARGS_Parsed!e

Reads and checks the whole command line against the program's description of it, and returns it as data: the subcommand, the positional arguments, the values and the flags. Errors on an unknown flag, a missing value, a value given to a flag that takes none, or a missing required option.

args_value(name:s):s!e

Returns the value of a flag like --name=value. Errors if the flag is missing.

args_value_float(name:s):f!e

Returns the value of a flag read as a fraction.

args_value_int(name:s):i!e

Returns the value of a flag read as a whole number. A missing flag and a value that is not a number are different errors.

args_value_or(name:s, fallback:s):s

Returns the value of a flag, or the fallback when it was not passed.

args_wants_help():b

Returns whether the program was asked for help with --help or -h. Check this first and print args_help_text.

array 77 functions
array_all_equal(array:a:T):b

Returns true if every element equals the first one, and true for an empty array.

array_binary_search(array:a:T, item:T):i!e

Returns where the item sits in an already sorted array, found by halving the range rather than walking it. Errors when the array does not contain it. An unsorted array gets a wrong answer rather than an error, so use array_index_of when the order is not known.

array_cartesian_product(first:a:T, second:a:T):a:a:T!e

Returns every pairing of one element from each array, as two-element arrays, with the first array moving slowest. Sizes against colours, days against rooms.

array_chunk(array:a:T, size:i):a:a:T!e

Splits the array into chunks of the given size. Errors if size is not positive.

array_combinations(array:a:T, size:i):a:a:T!e

Returns every way of choosing that many elements, order not counting. Refuses a request that would build more than a million arrays.

array_common_prefix_length(first:a:T, second:a:T):i

Returns how many elements the two arrays share at their start.

array_compact_strings(array:a:s):a:s

Removes empty strings from the array, keeping everything else in order.

array_concat(first:a:T, second:a:T):a:T

Returns a new array containing all elements of the first array followed by the second.

array_contains(array:a:T, item:T):b

Returns true if the array contains the given item.

array_count_by(array:a:T, key:fn(T):K):h<K,i> where K is i, s or b

Returns how many elements share each key, which is array_group_by when only the sizes matter.

array_count_of(array:a:T, item:T):i

Returns how many times the item appears in the array.

array_count_runs(array:a:T):i

Returns how many runs of consecutive equal elements the array has (0 for an empty array).

array_deduplicate(array:a:T):a:T

Removes consecutive duplicate elements.

array_deduplicate_by(array:a:T, key:fn(T):K):a:T where K is i, s or b

Returns the array with later elements dropped when their key has been seen before, keeping the first of each and the order they came in. Where array_deduplicate compares whole elements, this compares one thing about them, the way deduplicating records by address or id does.

array_difference(first:a:T, second:a:T):a:T

Returns the elements of the first array that are not in the second.

array_ends_with(array:a:T, suffix:a:T):b

Returns true if the array ends with the given suffix array, element for element.

array_find(array:a:T, value:T):i!e

Returns the index of the first occurrence of the value, or an error if not found.

array_find_last(array:a:T, value:T):i!e

Returns the index of the last occurrence of the value, or an error if not found.

array_first(array:a:T):T!e

Returns the first element, or an error if the array is empty.

array_flatten(array:a:a:T):a:T

Flattens a nested array by one level.

array_get(array:a:T, index:i):T!e

Returns the element at the given index, or an error if the index is out of bounds.

array_group_by(array:a:T, key:fn(T):K):h<K,a:T> where K is i, s or b

Buckets the elements by what the key function returns, keeping the order they appeared in inside each bucket. A key function returning true or false splits the array in two, which other languages call partition. For anything beyond bucketing, register the rows and write SQL.

array_index_of(array:a:T, item:T):i!e

Returns the index where the item first appears, or an error if the array does not contain it.

array_index_of_max(array:a:T):i!e

Returns the index of the largest element (the first one when tied). Errors if the array is empty.

array_index_of_min(array:a:T):i!e

Returns the index of the smallest element (the first one when tied). Errors if the array is empty.

array_insert(array:a:T, index:i, item:T):a:T!e

Returns a new array with the item inserted at the index, moving the rest along. Errors if the index is past the end.

array_insert_sorted(array:a:T, item:T):a:T

Returns the sorted array with one more item in it, still sorted. Keeps a leader board in order as scores arrive, without sorting the whole thing again.

array_insertion_point(array:a:T, item:T):i

Returns the position the item would take in a sorted array, which is also how many elements come before it. Asking a sorted list of prices how many are under twenty, without a pass over the list.

array_interleave(first:a:T, second:a:T):a:T

Alternates elements from the two arrays. When one runs out, the rest of the other follows.

array_intersect(first:a:T, second:a:T):a:T

Returns the elements present in both arrays, without duplicates.

array_is_empty(array:a:T):b

Returns true if the array has no elements.

array_is_sorted(array:a:T):b

Returns true if each element is less than or equal to the next, and true for an empty array.

array_is_unique(array:a:T):b

Returns true if no value appears more than once, and true for an empty array.

array_join(array:a:T, separator:s):s

Converts each element to a string and joins them with the separator.

array_last(array:a:T):T!e

Returns the last element, or an error if the array is empty.

array_length(array:a:T):i

Returns the number of elements in the array.

array_max(array:a:T):T!e

Returns the largest element, or an error if the array is empty.

array_max_by(array:a:T, key:fn(T):K):T!e

Returns the element whose key is largest. An empty array is an error.

array_middle(array:a:T):T!e

Returns the middle element (the lower of the two middles when the length is even). Errors if the array is empty.

array_min(array:a:T):T!e

Returns the smallest element, or an error if the array is empty.

array_min_by(array:a:T, key:fn(T):K):T!e

Returns the element whose key is smallest. An empty array is an error.

array_pad_end(array:a:T, length:i, value:T):a:T

Appends the value until the array reaches the length. An array already that long comes back unchanged.

array_pad_start(array:a:T, length:i, value:T):a:T

Prepends the value until the array reaches the length. An array already that long comes back unchanged.

array_page(array:a:T, page:i, per_page:i):a:T!e

Returns one page of the array, with pages numbered from 1. A page past the end is empty rather than an error, so a stale link shows nothing instead of breaking. Errors only when the page number or page size makes no sense.

array_permutations(array:a:T):a:a:T!e

Returns every ordering of the elements. Ten elements have three and a half million orderings, so anything that large is refused rather than attempted.

array_pop(array:a:T):a:T!e

Returns a new array with the last element removed. Errors if the array is empty.

array_push(array:a:T, item:T):a:T

Returns a new array with the item appended to the end.

array_range(start:i, end:i):a:i

Returns integers from start (inclusive) to end (exclusive).

array_range_inclusive(start:i, end:i):a:i

Returns integers from start to end, both inclusive.

array_remove_at(array:a:T, index:i):a:T!e

Returns a new array without the element at the index. Errors if the index is out of bounds.

array_repeat(value:T, count:i):a:T

Returns an array containing the value repeated count times.

array_replace_at(array:a:T, index:i, item:T):a:T!e

Returns a new array with the element at the index replaced. Errors if the index is out of bounds.

array_reverse(array:a:T):a:T

Returns a new array with the elements in reverse order.

array_rotate(array:a:T, count:i):a:T

Rotates elements by count positions (positive rotates right, negative rotates left).

array_rotate_left(array:a:T, count:i):a:T

Rotates elements count positions to the left.

array_rotate_right(array:a:T, count:i):a:T

Rotates elements count positions to the right.

array_shuffle(array:a:T):a:T

Returns a new array with the elements in random order.

array_skip(array:a:T, count:i):a:T

Returns a new array without the first count elements.

array_skip_last(array:a:T, count:i):a:T

Returns a new array without the last count elements.

array_skip_while(array:a:T, skip:fn(T):b):a:T

Returns the rest of the array, from the first element the named function says no to onwards. The other half of array_take_while - the two together put the array back.

array_slice(array:a:T, start:i, end:i):a:T!e

Returns elements from start (inclusive) to end (exclusive), or an error if out of bounds.

array_sort(array:a:T):a:T

Returns a new array sorted in ascending order.

array_sort_by(array:a:T, key:fn(T):K):a:T

Returns the array sorted by what the named key function returns for each element, smallest first. The sort is stable, so elements with equal keys keep the order they came in. That is how to sort on more than one key: sort by the least important key first and the most important key last.

array_sort_by_descending(array:a:T, key:fn(T):K):a:T

Returns the array sorted by the key function, largest first. Stable in the same way, and it reverses the order of the keys rather than the order of the ties, so one key can point down and another up in a stacked sort.

array_sort_descending(array:a:T):a:T

Returns a new array sorted from largest to smallest.

array_sort_natural(array:a:s):a:s

Sorts text the way a person reads names with numbers in them, so file2 comes before file10 instead of after it. Case is ignored, and names that differ only in case are settled by the text itself so the order never depends on the input order.

array_starts_with(array:a:T, prefix:a:T):b

Returns true if the array begins with the given prefix array, element for element.

array_step_by(array:a:T, step:i):a:T!e

Returns every step-th element starting with the first. Errors if step is less than 1.

array_sum(array:a:T):T

Returns the sum of all elements (0 for an empty array).

array_sum_by(array:a:T, key:fn(T):K):K where K is i or f

Returns every element's key added up, which is the total of a field over the array. An empty array sums to zero.

array_swap(array:a:T, first:i, second:i):a:T!e

Returns a new array with the two elements exchanged. Errors if either index is out of bounds.

array_take(array:a:T, count:i):a:T

Returns a new array with the first count elements (fewer if the array is shorter).

array_take_last(array:a:T, count:i):a:T

Returns a new array with the last count elements, in their original order (fewer if the array is shorter).

array_take_while(array:a:T, keep:fn(T):b):a:T

Returns the front of the array, up to the first element the named function says no to. Different from filter, which takes every element that passes wherever it sits - this stops at the first failure and ignores the rest.

array_union(first:a:T, second:a:T):a:T

Returns all unique elements from both arrays.

array_windows(array:a:T, size:i):a:a:T!e

Returns every run of neighbouring elements of that size, one step apart, so [1, 2, 3] in twos gives [1, 2] and [2, 3]. What a moving average or a three-in-a-row check reads. array_chunk is the one that cuts into pieces that do not overlap.

array_zip_with(first:a:A, second:a:B, combine:fn(A,B):C):a:C!e

Walks two arrays in step and returns what the named function makes of each pair. Errors if the arrays are different lengths, since two lists meant to line up and not lining up is a bug worth hearing about.

audio 5 functions
audio_is_available():b

Returns whether this machine has a sound device to play through. Ask before playing anything on a server, where the answer is usually no.

audio_play_file(path:s):v!e

Plays a sound file and returns when it has finished. WAV, MP3, FLAC and Ogg Vorbis are understood. To carry on while it plays, run it in a c block beside the other work.

audio_play_tone(hertz:f, seconds:f, volume:f):v!e

Plays a single tone and returns when it has finished. 440.0 hertz is a concert A. A volume of 0.2 is a better starting point for a notification than 1.0.

audio_tone_after(hertz:f, seconds:f, volume:f, wait_seconds:f):v!e

Starts a tone after a wait and returns at once. Several of these with growing waits are a short tune, queued in one go and played in time without the program keeping a clock of its own.

audio_tone_start(hertz:f, seconds:f, volume:f):v!e

Starts a tone and returns at once, without waiting for it to finish. This is the one a game loop can call, since waiting out a sound would stutter the picture. In a browser the page stays silent until the player has first touched or clicked it, which is the browser's rule, not an error.

base32 4 functions
base32_decode(text:s):s!e

Base32 back to text. Case and padding are forgiven. Characters outside the alphabet are not.

base32_decode_hex(text:s):s!e

Base32 back to hex bytes, for secrets that were never text.

base32_encode(text:s):s

Text as base32, RFC 4648 - the alphabet authenticator apps and DNS records use.

base32_encode_hex(hex:s):s!e

Hex bytes as base32 - how a binary secret becomes the code an authenticator app accepts.

base58 4 functions
base58_decode(text:s):s!e

Base58 back to text. Whitespace is forgiven. Characters outside the alphabet are not.

base58_decode_hex(text:s):s!e

Base58 back to hex bytes, for ids and keys that were never text.

base58_encode(text:s):s

Text as base58, the Bitcoin alphabet that leaves out 0, O, I and l so nothing is misread.

base58_encode_hex(hex:s):s!e

Hex bytes as base58, how a binary id becomes something short enough to read aloud. Leading zero bytes come out as leading 1s.

base64 4 functions
base64_decode(data:s):s!e

Decodes standard base64 back to text. Errors on invalid base64 or non-text bytes.

base64_decode_url(data:s):s!e

Decodes URL-safe base64, padded or not, back to text.

base64_encode(text:s):s

Encodes text as base64 with the standard alphabet and padding.

base64_encode_url(text:s):s

Encodes text as URL-safe base64 without padding, the form JWTs and URLs use.

binary 9 functions
binary_byte_length(hex:s):i!e

How many bytes a hex string holds - half its digit count.

binary_concat(parts:a:s):s!e

Joins hex pieces into one, checking each is real hex on the way.

binary_pack_float(value:f, big_endian:b):s

A float as its 8 hex bytes, IEEE 754 double precision.

binary_pack_float32(value:f, big_endian:b):s

A float as its 4 hex bytes - the single-precision form binary formats mostly use.

binary_pack_int(value:i, byte_count:i, big_endian:b):s!e

An integer as hex bytes: 1, 2, 4 or 8 of them, big- or little-endian. Refuses a value that does not fit the width.

binary_slice(hex:s, offset:i, length:i):s!e

A run of bytes out of the middle of hex data. Offset and length count bytes, not digits.

binary_unpack_float(hex:s, offset:i, big_endian:b):f!e

Reads an 8-byte float out of hex bytes at a byte offset.

binary_unpack_float32(hex:s, offset:i, big_endian:b):f!e

Reads a 4-byte float out of hex bytes at a byte offset.

binary_unpack_int(hex:s, offset:i, byte_count:i, big_endian:b, signed:b):i!e

Reads an integer out of hex bytes at a byte offset. Signed reads sign-extend two's-complement values.

bits 20 functions
bits_and(left:i, right:i):i

Returns the bits set in both numbers.

bits_count_ones(value:i):i

Returns how many bits are set, which is the size of a set held as a bitmask.

bits_count_zeros(value:i):i

Returns how many bits are clear.

bits_extract(value:i, offset:i, width:i):i!e

Reads a bit field out of the number, width bits starting at the offset. Errors when the field does not fit inside the 64 bits.

bits_from_binary(text:s):i!e

Reads a string of ones and zeros back into a number. Underscores are allowed as separators.

bits_get(value:i, index:i):b!e

Returns whether one particular bit is set, counting from 0 at the lowest. Errors outside 0 to 63.

bits_insert(value:i, offset:i, width:i, field:i):i!e

Writes a bit field into the number, width bits starting at the offset. Errors when the field does not fit its width or runs past bit 63.

bits_leading_zeros(value:i):i

Returns how many zero bits sit above the highest set bit, and 64 for zero itself.

bits_not(value:i):i

Returns the number with every bit flipped.

bits_or(left:i, right:i):i

Returns the bits set in either number.

bits_parity(value:i):i

Returns 1 when the count of one-bits is odd and 0 when it is even.

bits_rotate_left(value:i, places:i):i!e

Shifts the bits up, with the bits that fall off the top returning at the bottom.

bits_rotate_right(value:i, places:i):i!e

Shifts the bits down, with the bits that fall off the bottom returning at the top.

bits_set(value:i, index:i, on:b):i!e

Returns the number with one particular bit turned on or off. Errors outside 0 to 63.

bits_shift_left(value:i, places:i):i!e

Shifts the bits up, filling with zeros. Errors on a shift outside 0 to 63.

bits_shift_right(value:i, places:i):i!e

Shifts the bits down, filling with zeros. Errors on a shift outside 0 to 63.

bits_to_binary(value:i):s

Writes the bit pattern as ones and zeros, highest bit first, with no leading zeros.

bits_to_hex(value:i):s

Writes the bit pattern in hex, highest digit first, with no leading zeros.

bits_trailing_zeros(value:i):i

Returns how many zero bits sit below the lowest set bit, and 64 for zero itself.

bits_xor(left:i, right:i):i

Returns the bits set in one number but not the other.

bool 1 functions
bool_from(value:T):b!e where T is i, f, s or b

Reads a value as true or false. Text may be true, yes, y, on or 1 and their opposites false, no, n, off or 0, in any case. A number must be 1 or 0. Anything else is an error rather than a guess.

cache 8 functions
cache_clear(cache:s):v

Drops everything in one cache.

cache_delete(cache:s, key:s):v

Drops one key. Deleting what is not there is fine.

cache_get(cache:s, key:s):s!e

The stored value. An error when nothing is there or it has expired.

cache_get_or(cache:s, key:s, fallback:s):s

The stored value, or the fallback when nothing is there.

cache_has(cache:s, key:s):b

Whether a live value is stored under the key.

cache_len(cache:s):i

How many live values a cache holds.

cache_set(cache:s, key:s, value:s):v

Stores a value in a named in-memory cache. Caches live for the length of the process and are shared everywhere by name - this is how a web handler keeps something between requests. Values are strings: json_serialize anything richer first.

cache_set_ttl(cache:s, key:s, value:s, ttl_seconds:i):v

Stores a value that quietly disappears after the given number of seconds.

chart 7 functions
chart_bar(width:f, height:f, values:a:f, labels:a:s, colour:s, title:s):s!e

Returns an SVG bar chart, one bar per value, with the axis always including zero so the bars are comparable.

chart_donut(labels:a:s, values:a:f):s!e

Returns an SVG donut chart - a pie with a hole - with the total written in the middle.

chart_histogram(values:a:f, bins:i):s!e

Returns an SVG histogram of the values in 1 to 100 equal width bins, drawn as touching bars with the bin edges written along the x axis.

chart_line(width:f, height:f, values:a:f, labels:a:s, colour:s, title:s):s!e

Returns an SVG line chart of evenly spaced values. Labels are placed under the points at their own index, so fewer labels than points is fine.

chart_pie(labels:a:s, values:a:f):s!e

Returns an SVG pie chart of shares of a whole, one slice per value with a legend of names and percentages, colours dealt from a fixed palette in order.

chart_scatter(width:f, height:f, x_values:a:f, y_values:a:f, colour:s, title:s):s!e

Returns an SVG scatter plot, reading the two arrays together so the first x goes with the first y.

chart_sparkline(width:f, height:f, values:a:f, colour:s):s!e

Returns a small SVG line with no axis, labels or background, for putting a shape beside a number in a table or a line of prose.

code 3 functions
code_escape_html(text:s):s

Escapes &, <, >, " and ' so arbitrary text can be embedded safely in HTML, between tags or inside a quoted attribute.

code_highlight_html(source:s):s

Highlights Nail source as HTML using the real Nail lexer. Wraps tokens in <span class="tok-*"> elements for use inside <pre>.

code_transpile_to_rust(source:s):s!e

Runs the full Nail compiler pipeline (lex, parse, type check, transpile) on a source string and returns the generated Rust code.

color 14 functions
color_blue(color:s):i!e

The blue component of a hex color, 0 to 255.

color_contrast_ratio(first:s, second:s):f!e

The WCAG 2 contrast ratio between two colors, 1 (identical) to 21 (black on white). Accessibility guidelines ask for at least 4.5 for body text.

color_darken(color:s, amount:f):s!e

Blends a color toward black. Amount runs 0 (unchanged) to 1 (black).

color_grayscale(color:s):s!e

The color's brightness as an equal-component gray, using the ITU-R 601 luma weights.

color_green(color:s):i!e

The green component of a hex color, 0 to 255.

color_hsl(hue:f, saturation:f, lightness:f):s!e

Builds a hex color from the HSL wheel: hue in degrees (circular, so 380 reads as 20), saturation and lightness both 0 to 1.

color_invert(color:s):s!e

Flips every channel to its opposite: 255 minus each component.

color_is_dark(color:s):b!e

Whether the WCAG relative luminance falls below 0.5 - the cue for putting light text on it.

color_lighten(color:s, amount:f):s!e

Blends a color toward white. Amount runs 0 (unchanged) to 1 (white).

color_mix(first:s, second:s, share:f):s!e

Mixes two colors channel by channel. Share is how much of the second color, 0 to 1.

color_red(color:s):i!e

The red component of a hex color, 0 to 255. Accepts `#rrggbb` or 3-digit shorthand, with or without the `#`.

color_rgb(red:i, green:i, blue:i):s!e

Builds `#rrggbb` from red, green and blue components, each 0 to 255. A component outside that range is an error naming which one.

color_rotate_hue(color:s, degrees:f):s!e

Rotates a color around the HSL wheel by the given degrees, keeping its saturation and lightness - 180 lands on the complement, and stepping by 30 or 120 walks out a palette.

color_text_on(background:s):s!e

`#000000` or `#ffffff`, whichever has the higher contrast ratio against the given background.

compress 6 functions
compress_brotli(data:s):s!e

Brotli-compresses a string and returns it base64-encoded - what the web's `content-encoding: br` carries.

compress_gunzip(data:s):s!e

Decompresses a base64-encoded gzip string back to the original text.

compress_gzip(data:s):s!e

Gzip-compresses a string and returns it base64-encoded.

compress_unbrotli(data:s):s!e

Decompresses a base64-encoded brotli string back to the original text.

compress_unzstd(data:s):s!e

Decompresses a base64-encoded zstd string back to the original text.

compress_zstd(data:s):s!e

Zstd-compresses a string and returns it base64-encoded - the modern format for stored data, faster and tighter than gzip.

convert 2 functions
convert_fuel_economy(value:f, from_unit:CONVERT_FuelEconomy, to_unit:CONVERT_FuelEconomy):f!e

Fuel economy across its three dialects, named by CONVERT_FuelEconomy. Bigger mpg means less fuel, an inverse relation a factor table cannot hold, so this converts through liters per 100 km. The value must be positive.

convert_units(value:f, from_unit:CONVERT_Unit, to_unit:CONVERT_Unit):f!e

This number, in that unit. The units are CONVERT_Unit variants across length, mass, volume, area, speed, data, energy, power, pressure, frequency, angle and temperature, so a misspelled unit never compiles. Converting across dimensions is an error, not a guess.

crypto 29 functions
crypto_crc32(text:s):s

The CRC32 of text as 8 hex digits - the fast checksum zip and png use. A checksum catches accidents, not tampering. For tampering use a hash.

crypto_decrypt(encrypted:s, secret:s):s!e

Reads back what crypto_encrypt wrote, with the same secret. The wrong secret, text that was tampered with, and text that was never encrypted are all errors.

crypto_encrypt(text:s, secret:s):s!e

Encrypts text with a secret so only somebody holding the same secret can read it back, using AES-256-GCM. The result is URL-safe base64 and is different every time, and text changed afterwards fails to decrypt rather than decrypting to something else. For data at rest - a session cookie, a stored token. Passwords go through crypto_hash_password instead.

crypto_hash_blake3(input:s):s

The BLAKE3 of text as hex - the modern hash that is faster than the SHA family at the same strength. Good for content addressing and dedup keys.

crypto_hash_file_blake3(path:s):s!e

The BLAKE3 of a file's contents as hex, read in blocks so the file never has to fit in memory. The fast fingerprint for content addressing and dedup keys.

crypto_hash_file_sha256(path:s):s!e

The SHA-256 of a file's contents as hex, read in blocks so the file never has to fit in memory. The checksum a download is verified against.

crypto_hash_md5(input:s):s

Returns the MD5 hash of the input as a hex string (not for security-sensitive uses).

crypto_hash_password(password:s):s!e

Turns a password into something safe to store, using Argon2id with a fresh random salt. Never store a password with crypto_hash_sha256 - a graphics card guesses those billions of times a second.

crypto_hash_sha1(input:s):s

The SHA-1 of text as hex. Broken for new designs, still what git objects, OAuth 1 and older webhook signatures speak.

crypto_hash_sha256(input:s):s

Returns the SHA-256 hash of the input as a hex string.

crypto_hash_sha512(input:s):s

Returns the SHA-512 hash of the input as a hex string.

crypto_hmac_sha1(message:s, key:s):s

HMAC-SHA1 of a message as hex, for the older signature schemes that still ask for it. New designs use crypto_hmac_sha256.

crypto_hmac_sha256(key:s, message:s):s

Returns the HMAC-SHA256 of a message under a secret key, as hex. Verifies webhook signatures and signs values that pass through a browser.

crypto_hotp(secret_base32:s, counter:i):s!e

The six-digit code a base32 secret makes for a counter, RFC 4226. The counter-stepped cousin of TOTP, for hardware tokens and printed back-up code lists. The counter must not be negative.

crypto_random_hex(bytes:i):s!e

Returns the given number of operating-system random bytes as hex. Use this, not math_random, for session ids, nonces and anything an attacker must not guess.

crypto_random_id(length:i):s!e

Generates a random identifier of the given length using letters, digits, hyphen and underscore, so it needs no escaping in a URL.

crypto_secure_equal(left:s, right:s):b

Compares two secrets in time that does not reveal how much of them matched. Use it instead of == for session ids, tokens and signatures.

crypto_sign(signing_key:s, message:s):s!e

Signs a message with an Ed25519 signing key, as hex. The signature proves the message came from whoever holds that key and that not one character of it has changed since.

crypto_signing_key():s

Makes a new Ed25519 signing key, as hex - the secret half, which must never be published. Signing is what HMAC cannot do: whoever checks a signature does not need the secret and so cannot forge one. Keep the key in a file the service user owns and read it with env_get.

crypto_totp_at(secret_base32:s, timestamp:i):s!e

The code a secret made at a particular moment - what tests and audits ask for.

crypto_totp_now(secret_base32:s):s!e

The six-digit authenticator code a base32 secret makes right now, RFC 6238 - the same one the phone app shows.

crypto_totp_verify(secret_base32:s, code:s):b!e

Whether a code someone typed is the secret's current one. One clock step of drift on either side is forgiven, since phones and servers disagree by seconds.

crypto_ulid():s!e

Generates a ULID: 26 typable characters, sorted by the time it was made, with no hyphens. The identifier to put in a URL.

crypto_uuid_v4():s

Generates a random version 4 UUID string.

crypto_uuid_v5(namespace_uuid:s, name:s):s!e

A version 5 UUID, RFC 4122: the SHA-1 of a namespace UUID and a name folded into UUID shape. The same namespace and name always give the same id, which is the point - it turns any stable name into a stable UUID. The well-known DNS namespace is 6ba7b810-9dad-11d1-80b4-00c04fd430c8. Errors on a namespace that is not a UUID.

crypto_uuid_v7():s

Generates a version 7 UUID: random, but with the time it was made in the leading bits, so sorting the ids sorts them by age. The one to use for a database key.

crypto_verify_password(password:s, stored_hash:s):b!e

Checks a password against a hash from crypto_hash_password. False for a wrong password. A stored value that is not a hash at all is an error, because that row can never match any password, and a login that silently rejects forever is worth hearing about. To fail closed instead, use safe with false.

crypto_verify_signature(verifying_key:s, message:s, signature:s):b!e

Whether the signature really is this message signed by the holder of that verifying key. A key or signature of the wrong length is an error rather than a false, because that is a mistake in the program and not a message that failed its check.

crypto_verifying_key(signing_key:s):s!e

The verifying key that goes with a signing key, as hex. This is the half to publish: it goes in the program that checks signatures, or in the documentation of an API other people call.

csv 12 functions
csv_cell(text:s, header:s, row:i):s!e

Returns a single value by header name and zero based data row index, so row 0 is the first row after the header.

csv_close(reader:CSV_Reader):v!e

Closes a reader opened by csv_open and releases its file descriptor. A reader that is never closed holds its descriptor for the life of the process.

csv_column(text:s, header:s):a:s!e

Returns one column's values as strings, found by header name. A missing header is an error naming it and listing the columns the text has.

csv_default_options():CSV_Options

The default CSV options: comma separated, double-quoted, with a header row. Nail has no default field values, so this saves spelling out every field of CSV_Options.

csv_headers(text:s):a:s!e

Returns the first row's fields, which name the columns. Quote aware, so a header holding a comma inside quotes stays one field. Errors when the text is empty.

csv_next_rows(reader:CSV_Reader, count:i):a:h<s,s>!e

Reads up to `count` more rows from an open reader. A batch shorter than `count` means the file is finished, so callers loop until they get one.

csv_open(path:s, options:CSV_Options):CSV_Reader!e

Opens a CSV file for batch reading, for a file too large to hold in memory. Read from it with csv_next_rows and release it with csv_close.

csv_parse(text:s, options:CSV_Options):a:h<s,s>!e

Parses CSV text into one hashmap per row, keyed by the header row. Quote-aware, so a field containing the delimiter or a newline stays intact.

csv_row_count(text:s):i!e

Returns how many data rows the text has, not counting the header row. A newline inside a quoted field does not add a row.

csv_select_columns(text:s, headers:a:s):s!e

Returns a new CSV keeping only the named columns, in the order given, with quoting undone and redone properly. A missing header is an error naming it and listing the columns the text has.

csv_serialize(headers:a:s, rows:a:h<s,s>, options:CSV_Options):s!e

Writes rows out as CSV text, with the columns named and in the order given. Quotes any field holding the delimiter, a quote or a newline, and doubles a quote inside one. A row missing a column is written as an empty field.

csv_write(path:s, headers:a:s, rows:a:h<s,s>, options:CSV_Options):v!e

Writes rows straight to a file as CSV, with the same escaping as csv_serialize. The file is put in place by a rename, so a reader never catches it half written.

db_datafusion 7 functions
db_datafusion_close(db:DB_DataFusion):v!e

Close a DataFusion session

db_datafusion_execute(db:DB_DataFusion, sql:s):DB_DataFusion_Result!e

Execute a DataFusion SQL statement (CREATE TABLE, INSERT)

db_datafusion_query(db:DB_DataFusion, sql:s):a:T!e

Query DataFusion with SQL and return results as typed structs

db_datafusion_query_single(db:DB_DataFusion, sql:s):T!e

Query DataFusion with SQL and return a single result as a typed struct

db_datafusion_register_csv(db:DB_DataFusion, table:s, path:s):v!e

Register a CSV file as a queryable SQL table

db_datafusion_register_parquet(db:DB_DataFusion, table:s, path:s):v!e

Register a Parquet file as a queryable SQL table

db_datafusion_session():DB_DataFusion!e

Open an in-memory DataFusion analytics session

db_postgres 6 functions
db_postgres_close(db:DB_Postgres):v!e

Closes a connection and forgets its handle. Statements on it afterwards are an error rather than a hang.

db_postgres_connect(url:s):DB_Postgres!e

Connects to a Postgres server with a postgres:// connection string. The connection is not encrypted, so it belongs on localhost or a private network - across the internet, tunnel it.

db_postgres_execute(db:DB_Postgres, sql:s, params:a:s):DB_PostgresResult!e

Runs a statement that changes data, binding the values to $1, $2 and so on rather than putting them in the SQL text, and returns how many rows changed.

db_postgres_execute_batch(db:DB_Postgres, statements:s):v!e

Runs several statements in one round trip, for a schema created on startup. Nothing is bound, so nothing from outside the program belongs in the text.

db_postgres_query(db:DB_Postgres, sql:s, params:a:s):a:T!e

Returns every row of a query as the struct the assignment asks for, binding the values to $1, $2 and so on.

db_postgres_query_single(db:DB_Postgres, sql:s, params:a:s):T!e

Returns the one row a query returns. No rows or several rows are both errors, which makes this right for a lookup by key and wrong for a search.

db_sqlite 13 functions
db_sqlite_begin(db:DB_SQLite):v!e

Begin transaction (prefer db_sqlite_execute_batch for safer transactions)

db_sqlite_close(db:DB_SQLite):v!e

Close database connection

db_sqlite_commit(db:DB_SQLite):v!e

Commit transaction (prefer db_sqlite_execute_batch for safer transactions)

db_sqlite_execute(db:DB_SQLite, sql:s):DB_Result!e

Execute SQL statement

db_sqlite_execute_batch(db:DB_SQLite, statements:a:s):DB_Result!e

Execute multiple SQL statements in a single transaction (all succeed or all fail)

db_sqlite_execute_params(db:DB_SQLite, sql:s, params:a:s):DB_Result!e

Execute SQL with ? placeholders bound to values, so untrusted input never becomes part of the statement

db_sqlite_memory():DB_SQLite!e

Open in-memory SQLite database

db_sqlite_open(path:s):DB_SQLite!e

Open SQLite database

db_sqlite_query(db:DB_SQLite, sql:s):a:T!e

Query database and return results as typed structs

db_sqlite_query_params(db:DB_SQLite, sql:s, params:a:s):a:T!e

Query with ? placeholders bound to values and return the rows as typed structs

db_sqlite_query_single(db:DB_SQLite, sql:s):T!e

Query database and return single result as typed struct

db_sqlite_query_single_params(db:DB_SQLite, sql:s, params:a:s):T!e

Query with ? placeholders bound to values and return the first row as a typed struct

db_sqlite_rollback(db:DB_SQLite):v!e

Rollback transaction (prefer db_sqlite_execute_batch for safer transactions)

db_valkey 13 functions
db_valkey_close(connection:DB_Valkey):v!e

Forgets the connection. Closing twice is not an error.

db_valkey_connect(url:s):DB_Valkey!e

Connects to a Valkey server, or anything else speaking the open RESP protocol - the shared scratchpad between processes: sessions, counters, queues that several programs or machines read together. KeyDB and Dragonfly answer the same calls, and so do servers from the protocol's original lineage. For state one process keeps to itself, the cache module needs no server. URLs look like `redis://127.0.0.1/` or `redis://:password@host:6379/0`.

db_valkey_delete(connection:DB_Valkey, key:s):v!e

Drops a key. Deleting what is not there is fine.

db_valkey_exists(connection:DB_Valkey, key:s):b!e

Whether a key holds anything.

db_valkey_expire(connection:DB_Valkey, key:s, seconds:i):v!e

Gives an existing key a remaining life in seconds.

db_valkey_get(connection:DB_Valkey, key:s):s!e

The value under a key. An error when nothing is there.

db_valkey_increment(connection:DB_Valkey, key:s, by:i):i!e

Adds to a counter atomically and returns the new value. A key holding nothing starts at zero - which is how a rate limiter counts.

db_valkey_list_length(connection:DB_Valkey, key:s):i!e

How many values a list holds.

db_valkey_list_pop(connection:DB_Valkey, key:s):s!e

Takes the value at the front of a list - the consuming half of a work queue. An empty list is an error, so a worker loop uses safe().

db_valkey_list_push(connection:DB_Valkey, key:s, value:s):i!e

Pushes a value onto the end of a list and returns the new length - the producing half of a work queue.

db_valkey_publish(connection:DB_Valkey, channel:s, message:s):i!e

Sends a message to everyone subscribed to a channel, and answers how many heard it.

db_valkey_set(connection:DB_Valkey, key:s, value:s):v!e

Stores a value that stays until something deletes it.

db_valkey_set_ttl(connection:DB_Valkey, key:s, value:s, ttl_seconds:i):v!e

Stores a value that disappears after the given number of seconds - the shape sessions and rate limits take.

diff 3 functions
diff_apply(text:s, patch:s):s!e

Applies a patch diff_lines made (or git did) to a text, giving the new text. A patch that does not fit says where it failed instead of guessing.

diff_changed(old:s, new:s):b

Whether two texts differ at all - cheaper to ask than to render the diff.

diff_lines(old:s, new:s):s

The unified diff between two texts - the format git and code review read. Two equal texts diff to an empty patch body.

draw 19 functions
draw_arc(center_x:f, center_y:f, radius:f, start_degrees:f, end_degrees:f, color:s, stroke_width:f):s!e

An arc stroke along part of a circle, with 0 degrees at twelve o'clock and angles growing clockwise. A gauge is this arc twice - once faint for the track, once bright for the value.

draw_arrow(from_x:f, from_y:f, to_x:f, to_y:f, color:s, stroke_width:f):s!e

A line with a filled head at its far end, sized from the stroke width so a heavier arrow gets a bigger head.

draw_circle(center_x:f, center_y:f, radius:f, fill:s):s!e

A circle, given its centre and radius.

draw_ellipse(center_x:f, center_y:f, radius_x:f, radius_y:f, fill:s):s!e

An ellipse, given its centre and its two radii.

draw_grid(width:f, height:f, spacing:f, color:s):s!e

Evenly spaced guide lines in both directions across the given area, for laying a drawing out. Pass a light colour so the drawing stays on top.

draw_group(offset_x:f, offset_y:f, shapes:a:s):s

Several shapes moved together, which is how a chart's plotting area is kept clear of its labels without adding the margin to every coordinate by hand.

draw_line(x1:f, y1:f, x2:f, y2:f, stroke:s, stroke_width:f):s!e

A straight line between two points.

draw_path(commands:s, stroke:s, stroke_width:f, fill:s):s!e

An arbitrary path in SVG's own path notation - the escape hatch for a shape none of the others can make. An empty fill leaves it unfilled.

draw_polygon(points:a:f, fill:s):s!e

A closed shape through the given points, in the same flat array of x and y values.

draw_polyline(points:a:f, stroke:s, stroke_width:f):s!e

A run of connected line segments, given as a flat array of x and y values. This is the shape a line chart is made of.

draw_qr_svg(text:s):s!e

A QR code of the text as an SVG document, black on white. Put a URL in it and a phone camera opens the page - tickets, table menus, 2FA enrolment.

draw_rect(x:f, y:f, width:f, height:f, fill:s, corner_radius:f):s!e

A rectangle. A corner radius of 0.0 gives square corners.

draw_regular_polygon(center_x:f, center_y:f, sides:i, radius:f, fill:s):s!e

A regular polygon of 3 to 60 sides, every corner on one circle, drawn point up.

draw_rounded_rect(x:f, y:f, width:f, height:f, corner_radius:f, fill:s):s!e

A rectangle with rounded corners, the radius clamped to half the shorter side so a generous radius makes a capsule rather than a mess.

draw_scale(value:f, from_low:f, from_high:f, to_low:f, to_high:f):f!e

Moves a value from one range into another - the arithmetic every chart needs. To plot upward on a screen whose y grows downward, pass the height as to_low and 0.0 as to_high.

draw_star(center_x:f, center_y:f, points:i, outer_radius:f, inner_radius:f, fill:s):s!e

A star of 3 to 24 points, its corners alternating between the outer and inner radius, drawn point up.

draw_svg(width:f, height:f, background:s, shapes:a:s):s!e

Wraps shapes in an SVG document of the given size. An empty background leaves the drawing transparent. Save it with fs_write.

draw_text(x:f, y:f, content:s, size:f, fill:s, anchor:DRAW_Anchor):s!e

Text at a point. The anchor says which part of the text sits at that x - DRAW_Anchor::Middle is what a centred label wants.

draw_wedge(center_x:f, center_y:f, radius:f, start_degrees:f, end_degrees:f, fill:s):s!e

A filled slice of a circle between two angles, in the same clockwise degrees as draw_arc - the shape a pie chart is made of.

email 4 functions
email_default_server():EMAIL_Server

The details of a mail server filled in with what almost every provider wants - port 587 with TLS - so a program only sets what is different.

email_send(server:EMAIL_Server, to:s, subject:s, body:s):v!e

Sends a plain text message through an SMTP server and waits for it to be accepted. Success means the server took the message, not that it was delivered.

email_send_html(server:EMAIL_Server, to:s, subject:s, html:s):v!e

Sends an HTML message. Mail readers are far stricter than browsers, so this wants plain markup with inline styles rather than a page.

email_send_with_attachments(server:EMAIL_Server, to:s, subject:s, body:s, attachments:a:EMAIL_Attachment):v!e

Sends a plain text message with files attached - the invoice, the export, the report just made. An attachment's empty file_name shows the reader the file's own name, and an empty mime_type is guessed from the extension.

env 19 functions
env_all():h<s,s>

Returns every environment variable the process has, as a hashmap.

env_arch():s

Returns which processor this build is for: x86_64, aarch64, and so on.

env_args():a:s

Returns all command-line arguments, including the program name.

env_cache_dir(app_name:s):s!e

Where an app's disposable cache belongs - what can be deleted without losing anything.

env_config_dir(app_name:s):s!e

Where an app's configuration belongs on this system - ~/.config/<app> on Linux, the platform's own convention elsewhere. Not created automatically. fs_create_dir does that.

env_cpu_count():i

Returns how many processors the program may actually use - the number to size a worker pool by.

env_current_dir():s!e

Returns the directory the program is running in, which every relative path is relative to.

env_data_dir(app_name:s):s!e

Where an app's own data belongs - state worth keeping that is not configuration.

env_get(key:s):s!e

Returns the value of an environment variable. Errors if it is not set.

env_get_or(key:s, fallback:s):s

Returns the value of an environment variable, or the fallback when it is not set. A variable set to empty text counts as set and comes back empty.

env_home_dir():s!e

Returns the home directory of the user running the program. Errors when HOME is not set.

env_hostname():s!e

Returns the name of this machine, read from the kernel where possible.

env_load_dotenv(path:s):h<s,s>!e

Reads a .env file, sets every variable in it that is not already set, and returns what it read. Variables the process was started with always win.

env_os():s

Returns which operating system this build runs on: linux, macos, windows.

env_pid():i

Returns the process id of the running program - what goes in a pid file or a log line.

env_remove(key:s):v!e

Unsets an environment variable for this process. Removing one that was never set is not an error.

env_set(key:s, value:s):v!e

Sets an environment variable for the current process.

env_set_current_dir(path:s):v!e

Moves the program into another directory so relative paths resolve from there. Errors if it cannot be entered.

env_user():s!e

Returns the name of the user running the program. Errors when neither USER nor LOGNAME is set.

error 4 functions
danger(value:T!e):T

Unwraps a result, crashing the program if it is an error. Intended as a temporary escape hatch.

error_message(err:e):s

The text inside an error value, for handlers that want to show it or wrap it.

expect(value:T!e):T

Unwraps a result, crashing on error. Like danger, but signals the failure is considered impossible.

safe(value:T!e, handler:fn(e):T):T

Unwraps a result, calling the error handler to produce a fallback value on failure.

feed 4 functions
feed_atom(feed:FEED_Feed, entries:a:FEED_Entry):s!e

Builds an Atom 1.0 document from the same shapes feed_parse reads, the twin of feed_rss for the other format. Dates are RFC 3339, each entry is identified by its id or by its link when it has none, and a feed with no title or no link is an error rather than a feed nothing can follow.

feed_parse(text:s):FEED_Feed!e

Reads an RSS or Atom document into one shape, whichever it is: the feed's title and link, and its entries in the feed's own order. What a feed omits is empty rather than missing.

feed_rss(feed:FEED_Feed, entries:a:FEED_Entry):s!e

Builds an RSS 2.0 document from the same shapes feed_parse reads, so what one program publishes another parses straight back. Every value is XML-escaped, dates are written the RFC 2822 way RSS wants, a zero timestamp leaves the date off, and a feed with no title or no link is an error rather than a feed nothing can follow.

feed_sitemap(urls:a:s):s!e

Builds the sitemap a search engine reads to learn which pages exist without following every link to find them, from a list of absolute URLs. Duplicates are dropped and the order given is kept. A relative URL is an error, because a sitemap is read with no page to be relative to.

finance 10 functions
finance_cagr(beginning:f, ending:f, periods:i):f!e

Returns the compound annual growth rate in percent, the steady yearly rate carrying the beginning value to the ending value over the periods. Errors unless both values are positive and there is at least one period.

finance_compound(principal:f, rate_percent:f, compounds_per_year:i, years:f):f!e

Returns what a principal grows to under compound interest, with the yearly rate in percent compounded the given number of times a year for the given years. The rate is the yearly percentage as people quote it, 6.0 means six percent, matching money_loan_payment. Errors outside 1 to 366 compounds, on a rate at or below -100 or on negative years.

finance_effective_rate(nominal_percent:f, compounds_per_year:i):f!e

Returns the effective yearly rate in percent, what a nominal yearly rate quoted with compounding actually earns in a year, the APY behind an APR. Errors outside 1 to 366 compounds or on a nominal rate at or below -100.

finance_future_value(present_value:f, rate_percent:f, periods:i):f!e

Returns what a present amount grows to at the yearly rate over the given periods, compounded once per period. The rate is the yearly percentage as people quote it, 6.0 means six percent, matching money_loan_payment. Errors on a rate at or below -100 or negative periods.

finance_irr(cash_flows:a:f):f!e

Returns the internal rate of return in percent, the discount rate at which finance_npv of the flows is zero. Newton's method from ten percent with a bisection fallback over a scan from -99 to 10000 percent. Errors when the flows never change sign or when no rate in that range works.

finance_npv(rate_percent:f, cash_flows:a:f):f!e

Returns the net present value of the flows at the yearly discount rate in percent. The first flow sits at time zero undiscounted and each later flow is discounted one period more, the way a flow list starting with the up front investment reads. Errors on an empty array or a rate at or below -100.

finance_payback_periods(cash_flows:a:f):f!e

Returns how many periods the flows take to pay back the up front investment, interpolated linearly inside the period where the running total crosses zero. The first flow must be negative. Errors when the investment never pays back.

finance_present_value(future_value:f, rate_percent:f, periods:i):f!e

Returns what a future amount is worth today, discounted at the yearly rate over the given periods. The rate is the yearly percentage as people quote it, 6.0 means six percent, matching money_loan_payment. Errors on a rate at or below -100 or negative periods.

finance_roi_percent(cost:f, gain:f):f!e

Returns the return on investment in percent, the gain minus the cost as a share of the cost. Errors on a zero cost.

finance_rule_of_72_years(rate_percent:f):f!e

Returns the rule of 72 estimate of how many years money takes to double at the yearly rate in percent, 72 divided by the rate. Errors unless the rate is positive.

float 3 functions
float_approx_equal(first:f, second:f, tolerance:f):b

Returns whether two floats are within a tolerance of each other. This is how floats should be compared - 0.1 + 0.2 is not equal to 0.3, so == on computed floats is nearly always a bug.

float_from(value:T):f!e where T is i, f, s or b

Converts a value (string, int, etc.) to a float. Errors if it cannot be parsed.

float_is_whole(value:f):b

Returns whether the float holds a whole number exactly. Neither infinity nor not-a-number counts.

format 16 functions
format_bytes(count:i):s

Formats a byte count in the largest unit under 1024, like 1.5 KB.

format_clock(seconds:i):s

A duration in seconds as clock digits: m:ss under an hour, h:mm:ss from there, so 125 becomes 2:05. Negatives get a leading minus.

format_compact(value:i):s

Shortens a large count for display, so 1200 becomes 1.2k.

format_currency(amount:f, symbol:s):s

Formats an amount of money as the symbol followed by grouped digits and two decimals.

format_decimals(value:f, places:i):s!e

Formats a float with exactly the given number of decimal places, keeping trailing zeros. A negative count, or one above the 17 places a float can distinguish, is an error rather than a silent adjustment.

format_list(items:a:s, conjunction:s):s

Joins items the way a sentence would, as `a, b and c`, using the given conjunction.

format_number_words(value:i):s

A whole number in English words, so 42 becomes forty-two and -8000 becomes negative eight thousand. American style with no and, hyphenated twenty-one through ninety-nine, reaching the quintillions.

format_ordinal(number:i):s

Returns the English ordinal for a number, like 1st, 2nd or 13th.

format_parse_bytes(text:s):i!e

Reads a size written for people back into a count of bytes, so 1.5 KB is 1536 and 20 MB is twenty megabytes. This is what format_bytes wrote, and what a person types into a config file for a size limit. Steps of 1024, and KiB is another spelling of KB.

format_percent(fraction:f, places:i):s!e

Formats a fraction as a percentage, so 0.125 becomes 12.5%. A negative count of places, or one above the 17 a float can distinguish, is an error.

format_phone_na(digits:s):s!e

Formats a ten-digit North American phone number as (780) 555-0100, forgiving formatting characters and an eleventh leading 1. Any other count of digits is an error saying how many were found.

format_plural(count:i, singular:s, plural:s):s

Returns the count followed by the singular or plural word, whichever the count calls for.

format_roman(value:i):s!e

A number from 1 to 3999 as a Roman numeral, so 1994 becomes MCMXCIV. Errors outside that range, which Roman numerals cannot write.

format_significant(value:f, figures:i):s!e

Rounds a number to the given significant figures for display, so 1234.5 at 2 becomes 1200. Errors unless figures is 1 to 12.

format_thousands(value:i):s

Formats an integer with comma thousands separators.

format_thousands_float(value:f, places:i):s!e

Formats a float with comma thousands separators and a fixed number of decimal places. A negative count of places, or one above the 17 a float can distinguish, is an error.

fs 38 functions
fs_append(path:s, content:s):v!e

Adds to the end of a file, creating it if it is not there yet. Unlike fs_write, it keeps what the file already holds.

fs_append_file(from_path:s, to_path:s):v!e

Adds one file to the end of another, copying in blocks so neither has to fit in memory. How the pieces of a resumable upload are put back together.

fs_close(reader:FS_Reader):v!e

Closes a reader. Closing one that already reached the end is not an error.

fs_copy(from:s, to:s):v!e

Copies a file to a new location.

fs_create_dir(path:s):v!e

Creates a directory and any missing parent directories.

fs_dir_size(path:s):i!e

Returns how many bytes everything under a directory adds up to, however deep - what du reports. The file sizes are added rather than the blocks they occupy, and links are not followed.

fs_files_equal(first_path:s, second_path:s):b!e

Returns whether two files hold exactly the same bytes. Different lengths answer without reading either file, and files that differ early stop there rather than reading to the end.

fs_glob(directory:s, pattern:s):a:s!e

Returns every file at or below the directory whose path matches the glob pattern, sorted. The pattern is matched against the path below the directory.

fs_is_dir(path:s):b!e

Returns whether the path names a directory. False for a file and false for a path that is not there. A path that cannot be looked at, such as one inside a directory you may not read, is an error rather than false.

fs_is_executable(path:s):b!e

Whether a file can be run as a program. False for a directory or a missing file. A path that cannot be looked at is an error rather than false.

fs_is_file(path:s):b!e

Returns whether the path names a file. False for a directory and false for a path that is not there. A path that cannot be looked at is an error rather than false.

fs_modified(path:s):i!e

Returns when a file was last changed, as a Unix timestamp in seconds to compare with time_now.

fs_move(from:s, to:s):v!e

Moves (renames) a file to a new location.

fs_next_lines(reader:FS_Reader, count:i):a:s!e

The next lines from an open reader, without their line endings - at most count of them, and fewer at the end. An empty array means the file is finished and the reader has closed itself.

fs_open(path:s):FS_Reader!e

Opens a file for reading a piece at a time, for a file too large to hold in memory. Closed by fs_close, and closes itself once it reaches the end.

fs_read(path:s):s!e

Reads an entire file into a string. Errors if the file cannot be read.

fs_read_base64(path:s):s!e

A file's contents as base64 text - the way to get a file that is not text into a program, for a data: URI or a JSON field. A third larger than the bytes, so for small files.

fs_read_dir(path:s):a:s!e

Returns the sorted paths of everything directly inside a directory.

fs_read_lines(path:s):a:s!e

Reads a file and returns its lines with the line endings removed.

fs_read_range_base64(path:s, offset:i, length:i):s!e

A slice of a file as base64, for looking inside one without loading it. Fewer bytes come back if the file ends first.

fs_read_range_hex(path:s, offset:i, length:i):s!e

A slice of a file as hex, which is how a program works out what a file is: a PNG starts 89504e47, a zip 504b0304.

fs_read_with_encoding(path:s, encoding_label:s):s!e

Reads a file that is not UTF-8 - the windows-1252 CSV a bank exports, the shift_jis page an old site serves. Labels are WHATWG style: `windows-1252`, `shift_jis`, `utf-16le`, `euc-kr`.

fs_reduce_lines(path:s, initial:A, step:fn(A,s):A):A!e

Reads a file a line at a time and folds it into one value, the way reduce folds an array - so a file larger than memory can be counted, summed or searched. The step function takes what has been accumulated so far and the next line, and may read files or make requests itself.

fs_remove_dir(path:s):v!e

Removes an empty directory. A directory with anything in it is an error.

fs_remove_dir_all(path:s):v!e

Removes a directory and everything inside it. There is no undoing this.

fs_remove_file(path:s):v!e

Deletes a file. Errors if it does not exist or cannot be removed.

fs_set_executable(path:s, executable:b):v!e

Turns the executable bit on or off for a file - the step a program that writes a script has to take before it can run it.

fs_size(path:s):i!e

Returns how many bytes a file holds.

fs_tail_lines(path:s, count:i):a:s!e

The last lines of a file, read from the end - how a person looks at a log. Walked backwards in blocks, so a huge file costs only as much as the lines asked for.

fs_temp_dir():s

Returns the directory this machine keeps temporary files in. Nothing is created - join a name onto it with path_join.

fs_temp_file(prefix:s, extension:s):s!e

Creates a new empty file nobody else has in the temporary directory and returns its path, carrying the prefix and extension given.

fs_walk(path:s):a:s!e

Returns the sorted paths of every file underneath a directory, however deep. Directories themselves are not listed and links are not followed.

fs_watch_next(watcher:FS_Watcher, timeout_milliseconds:i):a:s!e

The paths that changed since the last call, waiting up to the timeout for the first change. An empty array means the time passed quietly.

fs_watch_start(path:s):FS_Watcher!e

Starts watching a file or directory for changes, directories all the way down. Changes pile up until fs_watch_next collects them, so nothing is missed between calls.

fs_watch_stop(watcher:FS_Watcher):v!e

Ends a watch and forgets its handle. Stopping one twice is not an error.

fs_write(path:s, content:s):v!e

Writes a string to a file, creating or truncating it.

fs_write_atomic(path:s, content:s):v!e

Writes a file by writing beside it and renaming into place, so a reader never sees a half-written file and a crash leaves the old one intact. The way to write a config, cache or state file.

fs_write_base64(path:s, data:s):v!e

Writes base64 text back out as the bytes it stands for. Text that is not base64 is an error rather than a file full of nonsense.

game 10 functions
game_circle(x:f, y:f, radius:f, color:s):GAME_Shape

A filled circle centred on x, y.

game_line(start_x:f, start_y:f, end_x:f, end_y:f, thickness:f, color:s):GAME_Shape

A straight line from one point to another, thickness pixels wide.

game_rect(x:f, y:f, width:f, height:f, color:s):GAME_Shape

A filled rectangle with its top left corner at x, y. Colours everywhere in this module are strings: #rrggbb, #rrggbbaa, #rgb, or a basic name like red.

game_rect_outline(x:f, y:f, width:f, height:f, thickness:f, color:s):GAME_Shape

Just the border of a rectangle, drawn thickness pixels wide.

game_run(config:GAME_Config, initial:T):T!e

Opens a window and runs a game until its view reports quit or the player closes the window, then returns the state it finished with. The program supplies two functions - view(state) returns a GAME_Frame and update(state, input) returns the next state. Input names keys as lowercase letters and digits plus Up, Down, Left, Right, Space, Enter, Esc, Shift, Ctrl, Alt, Tab and Backspace. A target_fps of 0 runs unpaced and an explicit target is honoured as written, however high - the engine imposes no ceiling of its own. In a browser, frames are paced by requestAnimationFrame regardless, so the display's refresh rate is the cap there.

game_sprite(handle:i, x:f, y:f):GAME_Shape

A loaded sprite drawn at its own size with its top left corner at x, y.

game_sprite_load(path:s):i!e

Reads a PNG from disk and returns the number that names it in game_sprite from then on. Load sprites once before game_run, not inside update or view.

game_sprite_scaled(handle:i, x:f, y:f, width:f, height:f):GAME_Shape

A loaded sprite stretched to width by height at x, y.

game_text(content:s, x:f, y:f, size:f, color:s):GAME_Shape

Text whose top left corner is at x, y, drawn size pixels tall in the built-in monospace font.

game_triangle(x1:f, y1:f, x2:f, y2:f, x3:f, y3:f, color:s):GAME_Shape

A filled triangle through three corners. The 3D module emits these, and they are just as usable straight from a program.

game3d 22 functions
game3d_default_environment():GAME3D_Environment

The environment most scenes want: an overhead sun a little to the side, white light, a sensible ambient floor, no fog. Nail structs have no default field values, so this saves spelling all eight out - a custom sky writes the GAME3D_Environment literal instead, the way a camera is written.

game3d_draw(mesh:i, position_x:f, position_y:f, position_z:f):GAME3D_Draw

One mesh placed in a scene at x, y, z: no spin, its own size, its own colours. The other game3d_draw functions reshape the value from there, so a program never writes the struct's seventeen fields by hand.

game3d_draw_glowing(draw:GAME3D_Draw, glow:f):GAME3D_Draw

The same draw lit from within, 0 to 1. At 0 the scene's light shades it like everything else, at 1 it shows its full colour whatever the light does - a sun, a lamp, lava. Glow burns through fog in the same proportion, so at 1 a distant sun stays blazing instead of greying into the haze, the way a light does in real fog.

game3d_draw_rotated(draw:GAME3D_Draw, rotation_x:f, rotation_y:f, rotation_z:f):GAME3D_Draw

The same draw spun to the given angles in radians, applied about x, then y, then z. Spinning about y alone is the usual turntable, x pitches forward and back, z rolls.

game3d_draw_scaled(draw:GAME3D_Draw, scale_x:f, scale_y:f, scale_z:f):GAME3D_Draw

The same draw stretched along each axis. The same number three times scales evenly, different numbers squash and stretch, and a unit ground mesh scaled by forty is a floor.

game3d_draw_shaded(draw:GAME3D_Draw, shader:GAME3D_Shader):GAME3D_Draw

The same draw painted by a shader loaded with `game3d_shader` instead of the builtin lighting. Draws sharing a mesh and a shader still land in one instanced draw call, so a whole ocean or a field of flames stays one batch.

game3d_draw_shader_params(draw:GAME3D_Draw, param_a:f, param_b:f, param_c:f, param_d:f):GAME3D_Draw

Four numbers handed to the draw's custom shader as `surface.params`, so one shader serves many draws: the same water rougher here and calmer there, the same fire hotter and colder. What each number means is the shader's own business. Draws sharing a mesh and a shader still batch whatever their params, the numbers ride the instance.

game3d_draw_tinted(draw:GAME3D_Draw, tint:s):GAME3D_Draw

The same draw repainted: every triangle takes this #rrggbb colour instead of the mesh's own, which is how one mesh serves as both the red team and the blue team. An empty string goes back to the mesh's colours.

game3d_line(camera:GAME3D_Camera, x1:f, y1:f, z1:f, x2:f, y2:f, z2:f, thickness:f, color:s):a:GAME_Shape

A line between two points in the world, projected onto the frame. The array is empty when the line is behind the camera, so it can always be concatenated into the shapes.

game3d_mesh(camera:GAME3D_Camera, handle:i, x:f, y:f, z:f, rotation_y:f, scale:f, tint:s):a:GAME_Shape!e

A loaded mesh seen through a camera: placed at x, y, z, spun rotation_y radians, scaled, lit by a fixed light and depth-sorted, returned as ordinary triangle shapes for the frame. An empty tint keeps the mesh's material colours, a #rrggbb tint repaints every triangle.

game3d_mesh_cube():i!e

A generated unit cube with a different colour on each face. Something to spin before any model has been found, and a fine building block after.

game3d_mesh_cylinder(color:s, sides:i):i!e

A generated cylinder of one colour standing on the y axis, one unit tall and half a unit across, with caps. `sides` is how many flat faces stand in for the curve, 3 to 64. Pillars, tree trunks, wheels lying down.

game3d_mesh_from_triangles(positions:a:f, colors:a:s):i!e

A mesh built straight from numbers: nine per triangle, three corners of x, y and z, wound counter-clockwise seen from outside, and exactly one colour per triangle - array_repeat turns one colour into a whole mesh's worth. Corners are kept exactly as given, no centring and no scaling, because whoever computes terrain knows where it goes. This is the raw material of anything procedural: heightfields, voxels, whole worlds from arithmetic.

game3d_mesh_ground(color_a:s, color_b:s, squares:i):i!e

A unit checkerboard, `squares` cells along each side alternating between the two colours, visible from above and below. Scale a draw of it up a hundred times and there is a floor with visible perspective for free, which is the fastest way to make a 3D scene read as a place.

game3d_mesh_load(path:s):i!e

Reads a glTF model and returns the number that names it from then on. On a real machine the path is a file on disk, in the browser build the same call fetches the path as a URL, so one program works in both worlds. Binary .glb files carry everything in one file and are the form to reach for. Textures are not read: triangles take their material's base colour, so low-poly models with coloured materials look best. Models are centred and scaled to one unit on load.

game3d_mesh_plane(color:s):i!e

A flat unit square of one colour lying in the ground plane, visible from above and below. Scaled up it is a floor, tilted it is a wall or a ramp.

game3d_mesh_sphere(color:s, bands:i):i!e

A generated sphere of one colour, half a unit across so it fills the same box a cube does. `bands` is how many horizontal slices build it: 3 is a gem, 24 is smooth, and anything outside 3 to 48 is clamped.

game3d_pick(ray:GAME3D_Ray, draws:a:GAME3D_Draw):i!e

Which draw a ray hits: the index into the array of the nearest mesh whose bounding box the ray passes through, or an error when it touches none of them. With the ray from game3d_ray under the mouse, this is clicking on a unit, and safe() turns the miss into whatever a miss means to the game.

game3d_project(camera:GAME3D_Camera, x:f, y:f, z:f):GAME3D_ScreenPoint

Where a world point lands on the screen, how far in front of the camera it sits, and whether it is really there to see. This is how a name tag, a health bar or a damage number drawn with 2D shapes follows something that lives in the 3D world.

game3d_ray(camera:GAME3D_Camera, screen_x:f, screen_y:f):GAME3D_Ray

The ray under a screen pixel: the camera's own position, and the direction of length one that pixel looks along. Feed the mouse to it and hand the result to game3d_pick, or intersect it with your own arithmetic for aiming, building placement, or anything else that starts at the screen and means the world.

game3d_scene(camera:GAME3D_Camera, environment:GAME3D_Environment, draws:a:GAME3D_Draw):GAME_Shape

A whole 3D scene as one shape for the frame: these meshes, placed like this, seen through this camera, lit like that. On a machine with a graphics card it renders there, depth buffered and instanced, and shapes before it in the frame stay under it while shapes after it draw over it, which is exactly where a HUD goes. Without a card it becomes depth-sorted triangles on the CPU, same picture, fewer frames. Build the scene fresh in view each frame - the frame draws it once and it is gone.

game3d_shader(source:s):GAME3D_Shader!e

Loads a surface shader from WGSL source, for `game3d_draw_shaded`. The source defines one function, `fn shade(surface: NAIL_Surface) -> vec4<f32>`, plus any helpers it wants. The surface carries `color` (the mesh colour with any tint mixed in), `normal`, `world_position`, `toward_eye`, the scene's `light` direction, `light_color` and `ambient`, `time` in seconds for animation, the draw's `glow`, and `params`, the draw's own four numbers from `game3d_draw_shader_params`. The engine keeps owning placement, instancing and fog - the shader only turns a surface into a colour. The WGSL is compiled here, once, so a typo fails at load with the compiler's message instead of at first draw. The CPU fallback renderer cannot run WGSL and shades such draws the builtin way.

geo 20 functions
geo_bearing(lat1:f, lon1:f, lat2:f, lon2:f):f!e

Returns the initial compass bearing from the first point toward the second, in degrees from 0 up to 360. Errors when a coordinate is off the map.

geo_bounds_east(longitudes:a:f):f!e

Returns the easternmost longitude in the array as a plain maximum, with the same honest antimeridian caveat as geo_bounds_west. Errors when the array is empty or holds a longitude off the map.

geo_bounds_north(latitudes:a:f):f!e

Returns the northernmost latitude in the array - the top edge of the points' bounding box. Errors when the array is empty or holds a latitude off the map.

geo_bounds_south(latitudes:a:f):f!e

Returns the southernmost latitude in the array - the bottom edge of the points' bounding box. Errors when the array is empty or holds a latitude off the map.

geo_bounds_west(longitudes:a:f):f!e

Returns the westernmost longitude in the array as a plain minimum. Points straddling the antimeridian get the honest numeric answer, a box reaching past 180 degrees wide, rather than a wrapped one. Errors when the array is empty or holds a longitude off the map.

geo_center(latitudes:a:f, longitudes:a:f):GEO_Point!e

Returns the center of the points as the mean of their 3D unit vectors brought back to the surface, correct across the antimeridian, as a GEO_Point. Errors when the arrays differ in length, are empty, hold a coordinate off the map, or the points balance out exactly.

geo_closest(latitude:f, longitude:f, latitudes:a:f, longitudes:a:f):i!e

Returns the index of the nearest point among parallel latitude and longitude arrays, ties going to the earlier index. Errors when the arrays differ in length, are empty, or hold a coordinate off the map.

geo_compass_point(bearing:f):s!e

Returns a bearing as its 16-wind compass name, `N` through `NNW`. Any finite number of degrees is accepted and normalized first. Only NaN or infinity errors.

geo_destination(latitude:f, longitude:f, bearing:f, distance_km:f):GEO_Point!e

Returns where you end up after traveling a distance in kilometers along a compass bearing, as a GEO_Point. Errors when the start is off the map or the distance is negative.

geo_distance_km(lat1:f, lon1:f, lat2:f, lon2:f):f!e

Returns the great-circle distance between two latitude/longitude points in kilometers, by the haversine formula on the WGS-84 mean earth radius. Errors when a coordinate is off the map.

geo_distance_miles(lat1:f, lon1:f, lat2:f, lon2:f):f!e

Returns the great-circle distance between two latitude/longitude points in statute miles. Errors when a coordinate is off the map.

geo_geohash(latitude:f, longitude:f, precision:i):s!e

Returns the point encoded as a geohash of the given precision, 1 to 12 characters of the standard base32 alphabet. Longer is a smaller cell. Errors when the coordinate is off the map or the precision is outside 1 to 12.

geo_geohash_decode(geohash:s):GEO_Point!e

Returns the center of the cell a geohash names, as a GEO_Point. Uppercase input is read as its lowercase self. Errors when the string is empty or holds a character outside the geohash alphabet.

geo_in_radius(lat1:f, lon1:f, lat2:f, lon2:f, radius_km:f):b!e

Returns whether the second point lies within the given great-circle distance of the first - the geofence question, with the fence line counting as inside. Errors when a coordinate is off the map or the radius is negative.

geo_midpoint(lat1:f, lon1:f, lat2:f, lon2:f):GEO_Point!e

Returns the geographic midpoint of two points, halfway along the great circle between them, as a GEO_Point. Errors when a coordinate is off the map.

geo_point_in_polygon(latitude:f, longitude:f, latitudes:a:f, longitudes:a:f):b!e

Returns whether the point lies inside the polygon traced by parallel latitude and longitude arrays - the neighborhood-boundary question, a point on the boundary counting as inside. The polygon closes itself from the last vertex back to the first. Errors when the arrays differ in length, hold fewer than 3 vertices, or hold a coordinate off the map.

geo_polygon_area_km2(latitudes:a:f, longitudes:a:f):f!e

Returns the area of the polygon in square kilometers, by the spherical shoelace formula. The winding direction does not matter, and the polygon closes itself from the last vertex back to the first. Errors when the arrays differ in length, hold fewer than 3 vertices, or hold a coordinate off the map.

geo_tile_x(longitude:f, zoom:i):i!e

Returns the OSM slippy-map tile column holding the longitude at the given zoom, 0 through 2 to the zoom minus 1. Errors when the longitude is off the map or the zoom is outside 0 to 22.

geo_tile_y(latitude:f, zoom:i):i!e

Returns the OSM slippy-map tile row holding the latitude at the given zoom, 0 at the top of the map. Latitudes beyond the Web-Mercator limit of 85.0511 degrees are clamped to it first, since the square map ends there. Errors when the latitude is off the map or the zoom is outside 0 to 22.

geo_valid(latitude:f, longitude:f):b

Returns whether the pair is a place on earth - latitude within -90 to 90 and longitude within -180 to 180. NaN is not a place.

graph 6 functions
graph_connected_components(edges_from:a:K, edges_to:a:K):a:a:K!e where K is i or s

The groups of nodes that touch through edges read in either direction, each group in its own array. Merging pairs into clusters, records that share an email, islands on a grid, are all this one call. Errors when the edge arrays differ in length.

graph_has_cycle(edges_from:a:K, edges_to:a:K):b!e where K is i or s

Whether following the edges around can ever come back to a node already passed through. The question to ask when a cycle is a real possibility rather than a mistake, since graph_topological_sort treats one as an error. Errors when the edge arrays differ in length.

graph_reachable(edges_from:a:K, edges_to:a:K, start:K):a:K!e where K is i or s

Every node the edges lead to from the start, following them one way only, the start itself first. Swapping the two edge arrays turns the question around into what reaches this node. Errors when the edge arrays differ in length or the start appears in no edge.

graph_shortest_path(edges_from:a:K, edges_to:a:K, start:K, goal:K):a:K!e where K is i or s

The route from start to goal that crosses the fewest edges, both ends included. Errors when the edge arrays differ in length, when either end appears in no edge, or when no route exists.

graph_shortest_path_weighted(edges_from:a:s, edges_to:a:s, weights:a:f, start:s, goal:s):GRAPH_Path!e

The cheapest route from start to goal when every edge carries a cost, with the route and its total together in a GRAPH_Path. One weight per edge, by position. Errors when the arrays differ in length, when a weight is negative or not a number, when either end appears in no edge, or when no route exists.

graph_topological_sort(edges_from:a:K, edges_to:a:K):a:K!e where K is i or s

An order that puts each edge's first node before its second. When every edge points from a prerequisite to the thing that needs it, this is the order to build, migrate or load in. Errors when the edge arrays differ in length, or when the edges loop, and the cycle error names the loop.

hashmap 26 functions
hashmap_add_to(map:h<K,i>, key:K, amount:i):i

Adds an amount to the running total under a key, starting from zero, and returns the new total. A negative amount subtracts.

hashmap_clear(map:h<K,V>):v

Removes all entries from the hashmap.

hashmap_contains_key(map:h<K,V>, key:K):b

Returns true if the hashmap contains the given key.

hashmap_entry_or_insert(map:h<K,V>, key:K, default:V):V

Returns the value for a key, inserting and returning the default if the key is missing.

hashmap_from_arrays(keys:a:K, values:a:V):h<K,V>!e

Builds a hashmap by pairing keys with values by position. Errors if the arrays are different lengths.

hashmap_get(map:h<K,V>, key:K):V!e

Returns the value for a key, or an error if the key is missing.

hashmap_get_or(map:h<K,V>, key:K, fallback:V):V

Returns the value for a key, or the fallback when the key is missing - never an error, and the map is not changed. The inserting cousin is hashmap_entry_or_insert.

hashmap_increment(map:h<K,i>, key:K):i

Adds one to the count under a key, starting from zero, and returns the new count.

hashmap_invert(map:h<K,V>):h<V,K> where V is i, s or b

Returns the hashmap turned around, so what were the values are the keys. Where two keys held the same value only one survives, so this is for lookups that go both ways, a code and its name.

hashmap_is_empty(map:h<K,V>):b

Returns true if the hashmap has no entries.

hashmap_key_of(map:h<K,V>, value:V):K!e

Returns a key holding the given value, or an error when none does - the lookup run backwards. With several such keys, which one comes back is undefined. Meant for values that appear once, the way an id does.

hashmap_keys(map:h<K,V>):a:K

Returns all keys in the hashmap as an array.

hashmap_keys_by_value(map:h<K,V>):a:K where K is i, s or b, V is i, f or s

Returns the keys ordered by the value each one holds, smallest first. Keys holding equal values come back in their own order, so two runs agree.

hashmap_keys_by_value_descending(map:h<K,V>):a:K where K is i, s or b, V is i, f or s

Returns the keys ordered by the value each one holds, largest first. This with array_take is the top ten: count with hashmap_increment, order here, take the front.

hashmap_len(map:h<K,V>):i

Returns the number of entries in the hashmap.

hashmap_max_by_value(map:h<K,V>):K!e where K is i, s or b, V is i, f or s

Returns the key holding the largest value, or an error when the hashmap is empty. Ties go to the first key in the keys' own order.

hashmap_merge(first:h<K,V>, second:h<K,V>):h<K,V>

Returns a new hashmap with entries from both maps. The second map wins on duplicate keys.

hashmap_min_by_value(map:h<K,V>):K!e where K is i, s or b, V is i, f or s

Returns the key holding the smallest value, or an error when the hashmap is empty. Ties go to the first key in the keys' own order.

hashmap_new():h<K,V>

Creates a new empty hashmap.

hashmap_omit(map:h<K,V>, keys:a:K):h<K,V>

Returns a new hashmap holding everything except the named keys - the other half of hashmap_pick. Dropping a password or a token before something is logged is what this is for.

hashmap_pick(map:h<K,V>, keys:a:K):h<K,V>

Returns a new hashmap holding only the named keys. A name that is not in the hashmap is simply not in the answer rather than an error.

hashmap_remove(map:h<K,V>, key:K):V!e

Removes a key and returns its value, or an error if the key is missing.

hashmap_set(map:h<K,V>, key:K, value:V):v

Inserts or updates a key-value pair in the hashmap.

hashmap_sorted_keys(map:h<K,V>):a:K where K is i, s or b

Returns the keys in order. A hashmap has no order of its own and hashmap_keys can hand them back differently between runs, so this is the one to use anywhere the order is seen.

hashmap_sum_values(map:h<K,V>):V where V is i or f

Returns the values added together. An empty hashmap totals zero, the way an empty array does.

hashmap_values(map:h<K,V>):a:V

Returns all values in the hashmap as an array.

hex 4 functions
hex_decode(data:s):s!e

Decodes hex back to text. Errors on non-hex characters or an odd length.

hex_dump(hex:s):s!e

Lays bytes out for a person: an offset column, 16 bytes of hex per line and an ASCII gutter with dots for the non-printable. Errors when the input is not hex.

hex_encode(text:s):s

Encodes text as hex, two lower-case characters per byte.

hex_xor(first:s, second:s):s!e

Returns the byte-wise xor of two hex strings of equal length. Errors name a length mismatch or bad hex.

html 11 functions
html_count(html:s, selector:s):i!e

Returns how many elements match the CSS selector.

html_images(html:s):a:s!e

Returns every image source on the page, in document order and exactly as written.

html_links(html:s):a:s!e

Returns every address an anchor on the page points at, in document order and exactly as written, so a relative link stays relative.

html_meta(html:s, meta_name:s):s!e

Returns the content of a meta tag by name, checking both the name and property spellings so Open Graph tags are found too.

html_sanitize(dirty:s):s

Cleans untrusted HTML so it is safe to serve: scripts, event handlers and javascript: links are removed, ordinary formatting is kept. Anything a person typed must pass through here - including markdown_to_html's rendering of it - before being put in a page.

html_select_attribute(html:s, selector:s, attribute:s):a:s!e

Returns one attribute of every matching element, skipping elements that do not carry it.

html_select_html(html:s, selector:s):a:s!e

Returns the markup inside every element matching the CSS selector.

html_select_text(html:s, selector:s):a:s!e

Returns the text of every element matching the CSS selector. A selector matching nothing gives an empty array.

html_text(html:s):s

Returns all the text of an HTML document with the tags removed and whitespace collapsed.

html_title(html:s):s!e

Returns the document's title, or an error when it has none - which usually means the page is not the page that was wanted.

html_to_markdown(document:s):s!e

The page as markdown, keeping headings, lists, links, emphasis and code and dropping the rest. The other direction from markdown_to_html, for a page fetched off the internet that has to be stored, diffed or searched. html_text is the one that leaves only the words.

http 25 functions
http_build_cookie(cookie:HTTP_Cookie):s!e

Builds the Set-Cookie header value for a cookie. Errors on a name, value or SameSite setting a browser would reject.

http_default_config():HTTP_Config

The default server configuration: no static mounts, 8 MiB body limit, 30 second handler timeout, empty state. Nail has no default field values, so this saves spelling out every field of HTTP_Config.

http_default_cookie(name:s, value:s):HTTP_Cookie

A cookie with the safe defaults filled in: site-wide path, session lifetime, HttpOnly, Secure, SameSite=Lax. Change the fields that need changing.

http_default_retry():HTTP_Retry

Retry settings worth having: three attempts, a wait starting at 250ms and doubling to at most 5s, and a 30s deadline for each attempt.

http_download_file(url:s, path:s):i!e

Downloads a URL straight into a file, streamed to disk piece by piece so the whole body is never in memory, and answers how many bytes were written. A response outside the 2xx range is an error naming the status, and a failure partway removes the partial file rather than leaving a half-download that looks whole.

http_live_count(channel:s):i

How many live subscribers a channel has right now.

http_live_send(channel:s, message:s):i

Sends a message to every SSE stream and websocket subscribed to the channel, returning how many there were. Nobody listening is 0, not an error.

http_multipart_extract(body_path:s, content_type:s, into_directory:s):h<s,s>!e

Takes a multipart/form-data body apart: file parts are written into the directory and text parts come back as values, in one hashmap where `name` is a value or a written path, `name.filename` is the cleaned-up name the client gave, and `name.type` is the declared content type. Read in blocks, so a large upload costs no more memory than a small one.

http_parse_cookies(header:s):h<s,s>

Parses the browser's Cookie header, which holds every cookie for the site at once, into a hashmap of name to value.

http_part_file(name:s, file_path:s):HTTP_Part

One file field of a multipart form. The file is read when the request is sent, so its bytes never have to pass through the program, and its name and media type are taken from the path.

http_part_text(name:s, value:s):HTTP_Part

One text field of a multipart form, the way a browser sends a filled-in text box.

http_path_matches(pattern:s, path:s):b

Whether a request path matches a route pattern. Pattern segments beginning with ':' match any single segment, and a trailing '*' matches the rest of the path.

http_path_params(pattern:s, path:s):h<s,s>!e

The named segments a route pattern binds, so `/dictionary/:word` against `/dictionary/cat` gives {word: cat}. A path the pattern does not match is an error, since a pattern that binds nothing also gives an empty map.

http_request(method:HTTP_Method, url:s, headers:h<s,s>, body:s):HTTP_Response!e

Makes an HTTP request (GET, POST, PUT, DELETE, or PATCH) and returns the response status and body.

http_request_multipart(method:HTTP_Method, url:s, headers:h<s,s>, parts:a:HTTP_Part):HTTP_Response!e

Sends a multipart/form-data request, the encoding file uploads use. Takes Post, Put or Patch, and sets Content-Type itself from the body's boundary, so headers must not carry one.

http_request_retry(method:HTTP_Method, url:s, headers:h<s,s>, body:s, retry:HTTP_Retry):HTTP_Response!e

Makes an HTTP request, sending it again while it fails in a way that might not fail next time: no answer at all, or a 408, 429, 500, 502, 503 or 504. Waits longer between attempts each time, honours a Retry-After header, and returns the last response whatever its status. The request is sent again unchanged, so an API that must not act twice wants an idempotency key in the headers.

http_server(port:i, config:HTTP_Config):v

Starts an HTTP server on the given port. Every request is passed to the program's handle_request(request:HTTP_Request, state:h<s,s>):HTTP_Response function, along with the config's state hashmap. Blocks forever.

http_server_realtime(port:i, config:HTTP_Config, live_path:s):v

http_server with a live endpoint beside the ordinary routes: a GET to live_path is a server-sent-event stream of everything http_live_send broadcasts, a websocket upgrade on the same path joins the same channel, and each text frame a client sends is answered by the program's handle_message function. ?channel=name picks the channel.

http_sse_close(events:HTTP_Events):v!e

Closes the stream and forgets the handle. Closing twice is not an error.

http_sse_connect(url:s, headers:h<s,s>):HTTP_Events!e

Opens a server-sent-events stream and holds it open - the streaming shape every model API answers with, where the body arrives a piece at a time. The headers are where an API key goes. A status that is not a success is an error rather than an empty stream.

http_sse_next(events:HTTP_Events, timeout_milliseconds:i):s!e

The data of the next event, waiting up to the timeout or forever when the timeout is 0. Comments and event names are skipped, and an event written over several data lines comes back as one string. The end of the stream is an error, so a loop reading until it fails is the shape that works.

http_ws_close(socket:HTTP_Websocket):v!e

Says goodbye properly and forgets the handle. Closing twice is not an error.

http_ws_connect(url:s):HTTP_Websocket!e

Opens a websocket to a ws:// or wss:// URL - the client half of http_server_realtime. This is how a program consumes a streaming API: an exchange feed, a chat bridge, another Nail program.

http_ws_receive(socket:HTTP_Websocket, timeout_milliseconds:i):s!e

The next text frame the other side sends. Waits up to the timeout, or forever when the timeout is 0. Pings are answered quietly. A closed connection is an error and forgets the handle.

http_ws_send(socket:HTTP_Websocket, text:s):v!e

Sends one text frame.

i18n 4 functions
i18n_load(directory:s):i!e

Loads every .json message catalog in a directory, once at startup. The file stem is the locale - en.json holds `en`, pt-BR.json holds `pt-BR` - and each file is one flat object of key to text. Returns how many messages were loaded.

i18n_locales():a:s

Every locale with a loaded catalog, for language pickers.

i18n_translate(locale:s, key:s):s

The message for a key in a locale. `pt-BR` falls back to `pt`, then to `en`, and a key nobody defines comes back as itself - visible in the page and greppable, never a crash.

i18n_translate_count(locale:s, key:s, count:i):s

The message for a count: `<key>.one` when the count is 1, `<key>.other` otherwise, with {count} in the text replaced by the number.

image 12 functions
image_blur(from_path:s, to_path:s, radius:f):v!e

Writes the picture blurred by that many pixels, for a background behind text or a preview that loads before the real thing. A larger radius is slower.

image_convert(from_path:s, to_path:s):v!e

Writes the picture in whatever format the written path's extension names.

image_crop(from_path:s, to_path:s, x:i, y:i, width:i, height:i):v!e

Writes the rectangle of the picture starting at that corner, measured from the top left in pixels. A rectangle reaching past the edge is an error rather than a smaller crop.

image_format(path:s):s!e

What format the file actually is, read from its bytes rather than its name - the check worth doing on an upload before storing it.

image_grayscale(from_path:s, to_path:s):v!e

Writes the picture in shades of grey, weighted the way an eye weighs colours rather than averaged, so the greys come out at the brightness the colours looked.

image_height(path:s):i!e

How many pixels tall the picture is.

image_mirror(from_path:s, to_path:s, mirror:IMAGE_Mirror):v!e

Writes the picture flipped over, left for right or top for bottom. What a selfie from a front camera needs, what a scan fed in face down needs, and what a sprite needs to walk the other way. A mirror is not a turn: it changes which hand somebody is waving with.

image_resize(from_path:s, to_path:s, width:i, height:i):v!e

Writes a copy of the picture at exactly that size. The written path's extension decides the format, so this converts as well as resizes.

image_resize_within(from_path:s, to_path:s, width:i, height:i):v!e

Writes a copy that fits inside the given box without stretching, so one side comes out smaller than asked for. A picture already smaller is copied at its own size.

image_rotate(from_path:s, to_path:s, turn:IMAGE_Turn):v!e

Writes the picture turned a quarter, a half or three quarters round, for a photograph that came off a phone on its side. Only the quarter turns exist: any other angle leaves empty corners nothing can fill sensibly.

image_thumbnail(from_path:s, to_path:s, size:i):v!e

Writes a square thumbnail filled edge to edge: the picture is scaled until it covers the square and the overhanging sides are cut off evenly, so a grid of these lines up whatever shape the pictures were.

image_width(path:s):i!e

How many pixels wide the picture is.

ini 6 functions
ini_get(text:s, section:s, key:s):s!e

The value under a section, with an empty section name meaning the top of the file. Inline comments are stripped, values trimmed, quoted values unquoted, and both `=` and `:` separate. A missing section or key is named in the error.

ini_has(text:s, section:s, key:s):b

Whether the section holds the key.

ini_keys(text:s, section:s):a:s!e

The keys of one section in order, with an empty section name meaning the top of the file. A missing section is named in the error.

ini_remove(text:s, section:s, key:s):s

The text with the key removed. Removing what is absent returns the text unchanged.

ini_sections(text:s):a:s

Section header names in order of first appearance, without duplicates.

ini_set(text:s, section:s, key:s, value:s):s

The text with the key set, replaced in place so order and comments survive. An absent key is appended to its section and an absent section is created at the end.

int 12 functions
int_abs(value:i):i!e

The size of an integer with its sign taken off. The most negative integer has no positive counterpart, so that one is an error.

int_clamp(value:i, low:i, high:i):i!e

Restricts an integer to the range low..high, both included. Errors if low is above high.

int_from(value:T):i!e where T is i, f, s or b

Converts a value (string, float, etc.) to an integer. Errors if it cannot be parsed.

int_from_hex(text:s):i!e

Reads a hexadecimal number, with or without the 0x in front. Errors if it is not one.

int_from_radix(text:s, base:i):i!e

Reads a number written in any base from 2 to 36, where digits above 9 are letters. Errors if it is not one.

int_is_even(value:i):b

Returns whether the integer divides evenly by two. Zero is even.

int_is_odd(value:i):b

Returns whether the integer leaves a remainder when divided by two.

int_max(first:i, second:i):i

The larger of two integers. The float version is math_max.

int_min(first:i, second:i):i

The smaller of two integers. The float version is math_min.

int_pow(base:i, exponent:i):i!e

Raises base to an integer power. Errors on negative exponents or overflow.

int_sign(value:i):i

Which side of zero an integer is on: -1 below, 1 above, 0 at zero. The float version is math_sign.

int_to_radix(value:i, base:i):s!e

Writes a number in any base from 2 to 36, using lower-case letters for digits above 9.

io 12 functions
io_confirm(question:s, default_answer:b):s!e

Asks a yes-or-no question until it gets an answer it understands, returning `yes` or `no`. An empty line means the default.

io_is_piped():b

Whether standard input is a pipe or a file rather than a person typing. Check this to decide between reading input and prompting for it.

io_read_all():s!e

Reads all of standard input to the end - what `cat data | program` hands over, and what makes a program usable in a pipe.

io_read_float():f!e

Reads a line from stdin and parses it as a float.

io_read_float_prompt(prompt:s):f!e

Prints a prompt, then reads a float from stdin.

io_read_int():i!e

Reads a line from stdin and parses it as an integer.

io_read_int_prompt(prompt:s):i!e

Prints a prompt, then reads an integer from stdin.

io_read_line():s!e

Reads a line from stdin (without the trailing newline). Errors if stdin is closed.

io_read_line_or(prompt:s, default_answer:s):s!e

Reads a line, returning the default when nothing is typed, so a setup script can be answered by holding down return.

io_read_line_prompt(prompt:s):s!e

Prints a prompt, then reads a line from stdin.

io_read_secret(prompt:s):s!e

Reads a line with nothing shown as it is typed, for a password or a token pasted into a terminal.

io_select(question:s, options:a:s):i!e

Shows a numbered list and asks until one is picked, returning the index of the chosen option.

json 22 functions
json_array_length(json:s, path:s):i!e

Returns how many items the list at a dotted path holds - the number to count up to when reading them one at a time. An empty path asks about the whole document.

json_compact(json:s):s!e

Returns the same JSON with every space between values taken out - the form to send or store.

json_count(json:s, path:s):i!e

Returns how many entries the object or list at a dotted path holds - fields for an object, items for a list. An empty path asks about the whole document.

json_deserialize(json_string:s):T!e

Deserialize a JSON string to a value (struct, enum, or array)

json_equal(first:s, second:s):b!e

Whether two pieces of JSON say the same thing, however they are written - spacing, indentation and the order of an object's fields do not count. Text that does not parse is an error naming which side it was, since a typo in one of them is not a difference between them.

json_flatten(json:s):s!e

Returns a nested object pressed into one flat object whose keys are dotted paths, so {"a":{"b":1}} becomes {"a.b":1} and a list contributes numbered segments like items.0. Errors if the document is not an object.

json_get_array_bools(json:s, path:s):a:b!e

Returns the list of true-or-false values at a dotted path. Each item is read the way json_get_bool reads one.

json_get_array_floats(json:s, path:s):a:f!e

Returns the list of numbers at a dotted path, whole or fractional. Each item is read the way json_get_float reads one.

json_get_array_ints(json:s, path:s):a:i!e

Returns the list of whole numbers at a dotted path. Each item is read the way json_get_int reads one: a number written as text is accepted, and a fraction is an error rather than a silent rounding.

json_get_array_strings(json:s, path:s):a:s!e

Returns the list of strings at a dotted path. Every item must be text - a list that mixes in numbers or objects is an error naming the first item that is not.

json_get_bool(json:s, path:s):b!e

Returns the true or false at a dotted path. The strings true and false count too.

json_get_float(json:s, path:s):f!e

Returns the number at a dotted path, whole or fractional.

json_get_int(json:s, path:s):i!e

Returns the whole number at a dotted path. A fraction is an error rather than a silent rounding.

json_get_string(json:s, path:s):s!e

Returns the text at a dotted path like user.name, where a number in the path indexes a list. A number or boolean there comes back as it was written. Errors if the path is not there.

json_has(json:s, path:s):b!e

Whether there is anything at a dotted path. A field that is present but null counts as missing, and a path that reaches nothing is false. Text that is not JSON is an error, because then there is no document to ask about.

json_keys(json:s):a:s!e

Returns the top-level field names of an object, sorted - for looking over an answer whose shape nobody wrote down.

json_merge(base:s, overlay:s):s!e

Returns two objects folded into one - the overlay's fields win, nested objects merge field by field, and lists and plain values are replaced whole. Errors unless both are objects.

json_pretty(json:s):s!e

Returns the same JSON indented, for a file a person will read or a diff that shows which field changed.

json_remove(json:s, path:s):s!e

Returns the document with the field at a dotted path dropped. Removing a field that was never there is fine.

json_serialize(value:Any):s!e

Serialize a value (struct, enum, or array) to a JSON string

json_set(json:s, path:s, value_json:s):s!e

Returns the document with the field at a dotted path set. The value is itself JSON, so `"hi"` sets text and 5 sets a number. Missing objects along the path are created. Walking through a plain value is an error.

json_type_of(json:s, path:s):s!e

Returns what kind of value sits at a dotted path - object, array, string, number, boolean or null. An empty path asks about the whole document. Errors if the path is not there.

jwt 4 functions
jwt_is_expired(token:s):b!e

Returns true if the token's expiry has passed, without checking the signature. For deciding whether to refresh a token, not whether to trust one.

jwt_read_unverified(token:s):s!e

Returns the claims of a token as JSON text without checking the signature. Nothing it returns has been verified.

jwt_sign(claims_json:s, secret:s, expires_in_seconds:i):s!e

Returns a signed HS256 token carrying the given JSON claims, expiring that many seconds from now, or never if the number is zero or less.

jwt_verify(token:s, secret:s):s!e

Returns the claims of a token whose signature checks out and whose expiry has not passed, as JSON text. Any other outcome is an error.

linalg 61 functions
linalg_mat3(values:a:f):LINALG_Mat3!e

A 3x3 matrix from exactly nine numbers, read left to right and top to bottom.

linalg_mat3_determinant(matrix:LINALG_Mat3):f

How much the transform multiplies area by. Zero means it flattens the plane onto a line, which is exactly when it cannot be undone.

linalg_mat3_equals(first:LINALG_Mat3, second:LINALG_Mat3, tolerance:f):b

True when two matrices match to within the given tolerance, which is how floats have to be compared once any arithmetic has happened to them.

linalg_mat3_get(matrix:LINALG_Mat3, row:i, column:i):f!e

One value out of the matrix, by row and column, both counted from 0.

linalg_mat3_identity():LINALG_Mat3

The transform that changes nothing - the one to start building from.

linalg_mat3_inverse(matrix:LINALG_Mat3):LINALG_Mat3!e

The transform that undoes this one. A transform that flattens the plane has no inverse, so that is an error.

linalg_mat3_multiply(first:LINALG_Mat3, second:LINALG_Mat3):LINALG_Mat3

Combines two transforms into one. The second happens first, which is the order the notation has meant since before computers.

linalg_mat3_rotation(radians:f):LINALG_Mat3

The transform that turns everything about the origin. To turn about another point, translate it to the origin first and back afterwards.

linalg_mat3_scaling(x:f, y:f):LINALG_Mat3

The transform that stretches everything about the origin, with a separate factor per axis.

linalg_mat3_to_array(matrix:LINALG_Mat3):a:f

All nine values as an array, read left to right and top to bottom.

linalg_mat3_transform_point(matrix:LINALG_Mat3, point:LINALG_Vec2):LINALG_Vec2

Moves a point through the transform, translation included.

linalg_mat3_transform_vector(matrix:LINALG_Mat3, vector:LINALG_Vec2):LINALG_Vec2

Moves a direction through the transform, ignoring translation - a direction has no position, so shifting one is always a mistake.

linalg_mat3_translation(x:f, y:f):LINALG_Mat3

The transform that moves everything by the given amounts.

linalg_mat3_transpose(matrix:LINALG_Mat3):LINALG_Mat3

The matrix with its rows and columns swapped.

linalg_vec2(x:f, y:f):LINALG_Vec2

A point or direction in the plane.

linalg_vec2_add(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2

Adds two vectors component by component - a position moved by a direction.

linalg_vec2_angle_between(first:LINALG_Vec2, second:LINALG_Vec2):f!e

The angle between two vectors in radians, from 0.0 to pi. A vector of zero length has no angle, so that is an error.

linalg_vec2_clamp(vector:LINALG_Vec2, low:LINALG_Vec2, high:LINALG_Vec2):LINALG_Vec2

Keeps a point inside a box, one component at a time.

linalg_vec2_distance(first:LINALG_Vec2, second:LINALG_Vec2):f

How far apart two points are.

linalg_vec2_divide(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2!e

Divides component by component. A zero component in the second vector is an error rather than an infinity.

linalg_vec2_dot(first:LINALG_Vec2, second:LINALG_Vec2):f

The dot product: positive when the vectors point the same way, zero when they are at right angles, negative when they oppose.

linalg_vec2_equals(first:LINALG_Vec2, second:LINALG_Vec2, tolerance:f):b

True when two vectors match to within the given tolerance, which is how floats have to be compared once any arithmetic has happened to them.

linalg_vec2_from_array(values:a:f):LINALG_Vec2!e

A vector from an array of exactly two numbers.

linalg_vec2_length(vector:LINALG_Vec2):f

How long the vector is.

linalg_vec2_length_squared(vector:LINALG_Vec2):f

The length multiplied by itself, without the square root. Comparing two of these answers which vector is longer for less work.

linalg_vec2_lerp(start:LINALG_Vec2, end:LINALG_Vec2, t:f):LINALG_Vec2

The point part of the way from start to end, with t clamped to 0.0..1.0.

linalg_vec2_max(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2

The larger of each component - the opposite corner of the box holding both points.

linalg_vec2_min(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2

The smaller of each component - one corner of the box holding both points.

linalg_vec2_multiply(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2

Multiplies two vectors component by component, which is a separate scale for each axis rather than any kind of vector product.

linalg_vec2_negate(vector:LINALG_Vec2):LINALG_Vec2

The vector of the same length pointing the opposite way.

linalg_vec2_normalize(vector:LINALG_Vec2):LINALG_Vec2!e

The vector of length one pointing the same way. A vector of zero length points in no direction, so that is an error.

linalg_vec2_perpendicular(vector:LINALG_Vec2):LINALG_Vec2

The vector at a right angle to this one, turned a quarter turn the way the coordinates grow.

linalg_vec2_reflect(vector:LINALG_Vec2, normal:LINALG_Vec2):LINALG_Vec2

Bounces a vector off a surface facing the given direction - where a ball goes when it hits a wall. The normal should have length one.

linalg_vec2_rotate(vector:LINALG_Vec2, radians:f):LINALG_Vec2

Turns a vector about the origin by an angle in radians.

linalg_vec2_scale(vector:LINALG_Vec2, factor:f):LINALG_Vec2

Multiplies both components by one number, which lengthens or shortens the vector without turning it.

linalg_vec2_subtract(first:LINALG_Vec2, second:LINALG_Vec2):LINALG_Vec2

Subtracts the second vector from the first - the direction from second to first.

linalg_vec2_to_array(vector:LINALG_Vec2):a:f

The two components as an array, in x, y order - the form draw_polyline and draw_polygon take.

linalg_vec2_zero():LINALG_Vec2

The origin: both components zero.

linalg_vec3(x:f, y:f, z:f):LINALG_Vec3

A point or direction in space.

linalg_vec3_add(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

Adds two vectors component by component - a position moved by a direction.

linalg_vec3_angle_between(first:LINALG_Vec3, second:LINALG_Vec3):f!e

The angle between two vectors in radians, from 0.0 to pi. A vector of zero length has no angle, so that is an error.

linalg_vec3_clamp(vector:LINALG_Vec3, low:LINALG_Vec3, high:LINALG_Vec3):LINALG_Vec3

Keeps a point inside a box, one component at a time.

linalg_vec3_cross(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

The vector at right angles to both - the direction a surface faces, given two directions lying in it.

linalg_vec3_distance(first:LINALG_Vec3, second:LINALG_Vec3):f

How far apart two points are.

linalg_vec3_divide(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3!e

Divides component by component. A zero component in the second vector is an error rather than an infinity.

linalg_vec3_dot(first:LINALG_Vec3, second:LINALG_Vec3):f

The dot product: positive when the vectors point the same way, zero when they are at right angles, negative when they oppose.

linalg_vec3_equals(first:LINALG_Vec3, second:LINALG_Vec3, tolerance:f):b

True when two vectors match to within the given tolerance, which is how floats have to be compared once any arithmetic has happened to them.

linalg_vec3_from_array(values:a:f):LINALG_Vec3!e

A vector from an array of exactly three numbers.

linalg_vec3_length(vector:LINALG_Vec3):f

How long the vector is.

linalg_vec3_length_squared(vector:LINALG_Vec3):f

The length multiplied by itself, without the square root. Comparing two of these answers which vector is longer for less work.

linalg_vec3_lerp(start:LINALG_Vec3, end:LINALG_Vec3, t:f):LINALG_Vec3

The point part of the way from start to end, with t clamped to 0.0..1.0.

linalg_vec3_max(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

The larger of each component - the opposite corner of the box holding both points.

linalg_vec3_min(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

The smaller of each component - one corner of the box holding both points.

linalg_vec3_multiply(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

Multiplies two vectors component by component, which is a separate scale for each axis rather than any kind of vector product.

linalg_vec3_negate(vector:LINALG_Vec3):LINALG_Vec3

The vector of the same length pointing the opposite way.

linalg_vec3_normalize(vector:LINALG_Vec3):LINALG_Vec3!e

The vector of length one pointing the same way. A vector of zero length points in no direction, so that is an error.

linalg_vec3_reflect(vector:LINALG_Vec3, normal:LINALG_Vec3):LINALG_Vec3

Bounces a vector off a surface facing the given direction - where a ball goes when it hits a wall. The normal should have length one.

linalg_vec3_scale(vector:LINALG_Vec3, factor:f):LINALG_Vec3

Multiplies every component by one number, which lengthens or shortens the vector without turning it.

linalg_vec3_subtract(first:LINALG_Vec3, second:LINALG_Vec3):LINALG_Vec3

Subtracts the second vector from the first - the direction from second to first.

linalg_vec3_to_array(vector:LINALG_Vec3):a:f

The three components as an array, in x, y, z order.

linalg_vec3_zero():LINALG_Vec3

The origin: all three components zero.

log 8 functions
log_debug(message:s):v

Writes a message to standard error at the Debug level, hidden unless the level is lowered to Debug.

log_error(message:s):v

Writes a message to standard error at the Error level.

log_info(message:s):v

Writes a message to standard error at the Info level, the default threshold.

log_set_file(path:s):v!e

Sends log lines to a file instead of standard error, for the rest of the run. The file is added to rather than replaced. Errors if the file cannot be opened, which is better found here than by losing lines later.

log_set_json(enabled:b):v

Switches log lines between a human-readable form and one JSON object per line, for the rest of the run.

log_set_level(level:LOG_Level):v

Hides every message below this level for the rest of the run. Info by default.

log_warn(message:s):v

Writes a message to standard error at the Warn level.

log_with_fields(level:LOG_Level, message:s, fields:h<s,s>):v

Writes a message with named values beside it, which is what makes a log line searchable rather than just readable.

markdown 9 functions
markdown_front_matter(document:s):h<s,s>

The key: value lines between a pair of --- fences at the top of a document, as a hashmap. A document with no front matter gives an empty one.

markdown_headings(markdown:s):a:s

Every heading's text in the document, in order, whatever its level.

markdown_links(markdown:s):a:s

Every link destination in the document, in the order the links appear.

markdown_to_html(markdown:s):s

Converts a Markdown string to HTML.

markdown_to_html_with_options(markdown:s, enable_tables:b, enable_footnotes:b, enable_strikethrough:b):s

Converts Markdown to HTML with tables, footnotes, and strikethrough toggled individually.

markdown_to_text(markdown:s):s

Strips a Markdown document to plain text: headings, bold and links keep their text, code blocks keep their code, list items keep their lines.

markdown_toc(markdown:s):s

A Markdown bullet list of the document's headings, indented two spaces per level below the top, each linking to its GitHub-style anchor.

markdown_without_front_matter(document:s):s

The document without its front matter, which is the part to render.

markdown_word_count(markdown:s):i

How many words the document's plain text holds, with the formatting not counted.

math 69 functions
math_abs(value:f):f

Returns the absolute value of a float.

math_acos(value:f):f!e

Returns the arccosine in radians. Errors if the input is outside -1.0..1.0.

math_acosh(value:f):f!e

Returns the inverse hyperbolic cosine. Errors if the input is below 1.0, where the function has no real answer.

math_asin(value:f):f!e

Returns the arcsine in radians. Errors if the input is outside -1.0..1.0.

math_asinh(value:f):f

Returns the inverse hyperbolic sine.

math_atan(value:f):f

Returns the arctangent in radians.

math_atan2(y:f, x:f):f

Returns the angle from the positive x axis to the point (x, y), from -pi to pi. Use this rather than math_atan for angles - a plain arc tangent cannot tell the quadrants apart.

math_atanh(value:f):f!e

Returns the inverse hyperbolic tangent. Errors at or outside -1.0 and 1.0, where the function has no finite answer.

math_cbrt(value:f):f

Returns the cube root, which is defined for negative numbers too - unlike raising to the power of one third.

math_ceil(value:f):f

Rounds up to the nearest whole number.

math_clamp(value:f, min:f, max:f):f

Restricts a value to the range min..max.

math_combinations(n:i, k:i):i!e

Returns how many ways to choose k things from n when order does not matter. Choosing more than there are gives 0, negatives and overflow are errors.

math_compound_growth(principal:f, rate_per_period:f, periods:i):f!e

Returns what a starting amount becomes after growing by a fixed rate for a number of periods. Errors on negative periods.

math_copysign(magnitude:f, sign_source:f):f

Returns the first number wearing the sign of the second.

math_cos(radians:f):f

Returns the cosine of an angle in radians.

math_cosh(value:f):f

Returns the hyperbolic cosine.

math_digit_count(value:i):i

Returns how many decimal digits a number has, ignoring its sign. 0 has one digit.

math_divide(numerator:T, denominator:T):T!e where T is i or f

Divides two numbers, returning an error on division by zero.

math_e():f

Returns Euler's number e (2.71828...).

math_erf(value:f):f

Returns the error function, the share of a Gaussian bell within the given distance of its centre, running from -1 to 1. Odd, so erf(-x) is exactly -erf(x).

math_erfc(value:f):f

Returns the complementary error function 1 - erf, computed directly so the tiny tail values for large inputs keep their accuracy instead of cancelling to noise.

math_exp(value:f):f

Returns e raised to the given power.

math_expm1(value:f):f

Returns e^x - 1, computed accurately for x very close to zero where subtracting 1 afterwards would cancel the precision away.

math_factorial(value:i):i!e

Returns value! as an integer. Errors for negative input or results that overflow.

math_fibonacci(position:i):i!e

Returns the Fibonacci number at a position, counting from fibonacci(0) = 0. Position 92 is the last that fits in a 64-bit integer.

math_floor(value:f):f

Rounds down to the nearest whole number.

math_fract(value:f):f

Returns just the fractional part, keeping the sign.

math_gcd(first:i, second:i):i

Returns the greatest common divisor of two integers.

math_hypot(x:f, y:f):f

Returns the distance from the origin to (x, y), computed without squaring the inputs first so very large distances stay exact.

math_is_finite(value:f):b

Returns whether this is an ordinary number - neither infinite nor not-a-number. The check to make before trusting a computed value.

math_is_infinite(value:f):b

Returns whether this is positive or negative infinity.

math_is_nan(value:f):b

Returns whether this is the not-a-number value. It is the one value not equal to itself, so == cannot be used to ask.

math_is_perfect_square(value:i):b

Returns whether the integer is some integer multiplied by itself. No negative number is.

math_is_prime(value:i):b

Returns true if the integer is a prime number.

math_lcm(first:i, second:i):i

Returns the least common multiple of two integers.

math_lerp(start:f, end:f, t:f):f

Linearly interpolates between start and end by t (clamped to 0.0..1.0).

math_log(value:f):f!e

Returns the natural logarithm. Errors if the input is not positive.

math_log10(value:f):f!e

Returns the base-10 logarithm. Errors if the input is not positive.

math_log1p(value:f):f!e

Returns ln(1 + x), computed accurately for x very close to zero where adding 1 first would lose it. Errors at or below -1.

math_log2(value:f):f!e

Returns the base-2 logarithm. Errors if the input is not positive.

math_log_base(value:f, base:f):f!e

Returns the logarithm in a base of your choosing. Errors on a value at or below zero or an unusable base.

math_max(first:f, second:f):f

Returns the larger of two floats.

math_min(first:f, second:f):f

Returns the smaller of two floats.

math_modulo(value:f, divisor:f):f!e

Returns the remainder with the sign of the divisor, so -1 modulo 12 is 11. What clock arithmetic and wrapping an index round an array need, and what % does not give.

math_nth_root(value:f, degree:i):f!e

Returns the nth root. An odd root of a negative number is negative, an even root of one is an error.

math_page_count(total_items:i, per_page:i):i!e

Returns how many pages a list of that many items fills, which is the last page number a listing can link to. A part-full last page counts, and an empty list is still one page.

math_percent_change(old:f, new:f):f!e

Returns how much a value grew or shrank as a percentage of where it started. Errors when the old value is zero.

math_percent_of(part:f, whole:f):f!e

Returns what percentage the part is of the whole. Errors when the whole is zero.

math_permutations(n:i, k:i):i!e

Returns how many ways to arrange k things drawn from n when order matters. Drawing more than there are gives 0, negatives and overflow are errors.

math_pi():f

Returns the constant pi (3.14159...).

math_pow(base:f, exponent:f):f

Raises base to the power of exponent.

math_random():f

Returns a random float between 0.0 (inclusive) and 1.0 (exclusive).

math_round(value:f):f

Rounds to the nearest whole number, returned as a float.

math_round_to(value:f, decimals:i):f!e

Rounds to a fixed number of decimal places (0 to 12), halves away from zero the way people round on paper.

math_round_to_int(value:f):i

Rounds to the nearest whole number, returned as an integer.

math_sigmoid(value:f):f

Returns the logistic sigmoid 1 / (1 + e^-x).

math_sign(value:T):i where T is i or f

Returns -1, 0 or 1 according to whether the value is negative, zero or positive.

math_sin(radians:f):f

Returns the sine of an angle in radians.

math_sinh(value:f):f

Returns the hyperbolic sine.

math_smoothstep(edge_low:f, edge_high:f, value:f):f!e

Eases from 0 at the low edge to 1 at the high edge along a smooth S-curve, holding at 0 and 1 outside them. The edges must differ.

math_sqrt(value:f):f

Returns the square root of a float.

math_sum_of_digits(value:i):i

Returns the decimal digits of a number added together, ignoring its sign.

math_tan(radians:f):f

Returns the tangent of an angle in radians.

math_tanh(value:f):f

Returns the hyperbolic tangent.

math_to_degrees(radians:f):f

Writes an angle given in radians as degrees.

math_to_radians(degrees:f):f

Writes an angle given in degrees as radians, which is what every function here that takes an angle expects.

math_triangular(n:i):i!e

Returns the nth triangular number 1 + 2 + ... + n. Errors on negative input or overflow.

math_trunc(value:f):f

Throws away the fractional part towards zero, so -2.7 becomes -2.0 where math_floor would give -3.0.

math_wrap(value:f, low:f, high:f):f!e

Folds a value into the range from low up to but not including high, the way an angle of 370 degrees is really 10. The low edge must be below the high.

mcp 1 functions
mcp_serve(name:s, version:s, tools:a:MCP_Tool):v!e

Serves the declared tools as an MCP server over stdin and stdout, the protocol AI assistants use to call outside tools. Each call is passed to the program's handle_tool(name:s, arguments_json:s):s!e function, whose Ok text becomes the tool result and whose error becomes a tool error the model can read. Stdout belongs to the protocol while serving, so anything for a person goes through the log functions, which write to stderr. Blocks until the client hangs up. The error case is a tool list that is empty or carries a schema that is not JSON.

mime 3 functions
mime_extension_for(media_type:s):s!e

The usual extension for a media type, without the dot, for naming a file that arrived with a type but no name.

mime_for_path(path:s):s

What a browser should be told a file is, worked out from its name. An unknown or missing extension gives application/octet-stream rather than a guess.

mime_is_text(media_type:s):b

Whether a media type is text a program could read as a string, which covers text/* and the structured formats spelled application/json and the like.

ml 12 functions
ml_cross_validate_boost(features:a:a:f, targets:a:f, folds:i, config:ML_BoostConfig, seed:i):ML_Regression!e

Trains and scores a boosted model once per fold, holding out a different slice each time, and averages the held-out scores. One split on a small dataset says as much about which rows landed where as about the model. This does not.

ml_encode_with(values:a:s, encoding:h<s,f>, fallback:f):a:f

Applies an encoding from ml_target_encode to a column. A category the encoding has never seen becomes the fallback, which should be the overall average of the training targets.

ml_kmeans(points:a:a:f, k:i, seed:i, iterations:i):ML_Clusters!e

Groups points by nearness into k groups. The starting points come from the seed and the answer depends on them, which is why the seed is an argument rather than a hidden decision.

ml_knn_predict(features:a:a:f, labels:a:i, query:a:f, k:i):i!e

Predicts a label by asking the k nearest rows what they are. No fitting happens - the data is the model - so this is what to reach for when there is very little of it.

ml_normalize(values:a:f):a:f!e

Rescales values so the smallest becomes 0.0 and the largest 1.0. Do this before any model that measures distance, so a column in millions does not drown out one in single digits.

ml_one_hot(values:a:s):ML_OneHot!e

Turns a column of words into one column of 0s and 1s per distinct word, with the sorted vocabulary that did it. Keep the vocabulary - new data must be encoded against the same one or every column shifts along.

ml_one_hot_with(values:a:s, categories:a:s):a:a:f!e

Encodes a column against a vocabulary already decided, so new data lines up with what a model was trained on. A word that was not in the training data becomes all zeros.

ml_regression_scores(predicted:a:f, actual:a:f):ML_Regression!e

Judges predicted numbers against real ones several ways at once: r_squared, mae, rmse, mape, median_ape and within_ten_percent. Rows whose real value is zero are left out of the percentage measures rather than making them infinite.

ml_score(predicted:a:i, actual:a:i):ML_Scores!e

Counts how a set of predictions did, treating the label 1 as positive. All four numbers come back together because accuracy alone flatters a model that never says yes.

ml_split_train_test(features:a:a:f, labels:a:i, train_share:f, seed:i):ML_Split!e

Cuts a dataset into a part to learn from and a part to be judged on, shuffling first so an ordering in the file does not become an ordering in the split. The seed makes the cut reproducible.

ml_standardize(values:a:f):a:f!e

Rescales values to sit around zero with a spread of one. The other way of putting columns on the same footing, and the one to use when outliers matter.

ml_target_encode(values:a:s, targets:a:f, smoothing:f):h<s,f>!e

Replaces each category with its average target, pulled towards the overall average according to how few rows it has. For columns where one-hot would add a thousand columns. Fit on training rows only - the smoothing is what stops a one-row category being encoded as its own answer.

ml_boost 6 functions
ml_boost_default_config():ML_BoostConfig

Sensible values to start a boosted model from: 100 trees, a learning rate slow enough that no single tree dominates, and a depth shallow enough to generalise.

ml_boost_fit(features:a:a:f, targets:a:f, config:ML_BoostConfig):ML_Boost!e

Fits a gradient boosting model - many small trees, each trained on what the ones before it still get wrong. Nail's own implementation of the technique LightGBM and XGBoost made famous - the method that wins on ordinary tabular data. Predicts a number. For yes-or-no questions fit against 0 and 1.

ml_boost_fit_validated(features:a:a:f, targets:a:f, validation_features:a:a:f, validation_targets:a:f, config:ML_BoostConfig):ML_Boost!e

Fits a boosted model while watching a held-out set, and stops once that set stops improving - the answer to the only hard question ml_boost_fit asks, which is how many trees. Trees grown after the best one are thrown away.

ml_boost_importance(model:ML_Boost):a:f!e

How much each column contributed, as a share of the total gain, in the original column order. A column near zero is one the model ignored, and dropping it costs nothing.

ml_boost_predict(model:ML_Boost, row:a:f):f!e

What a boosted model says about one row: the starting average plus every tree's correction.

ml_boost_predict_probability(model:ML_Boost, row:a:f):f!e

What a model fitted with ML_Objective::Logistic says, as a probability from 0.0 to 1.0. Refuses a model fitted to predict a number.

ml_forest 2 functions
ml_forest_fit(features:a:a:f, labels:a:i, trees:i, max_depth:i, seed:i):ML_Forest!e

Fits a forest of trees, each grown on a different random sample of the rows, that predict by voting. Far harder to get badly wrong than a single tree and far less sensitive to settings than boosting - reach for it when there is no time to tune anything.

ml_forest_predict(model:ML_Forest, row:a:f):i!e

What the forest says about one row: the answer most of its trees give.

ml_linear 2 functions
ml_linear_fit(features:a:a:f, targets:a:f):ML_Linear!e

Fits the straight line closest to the data, exactly rather than iteratively - no learning rate to tune. Errors when two columns say the same thing, because then no single line fits best.

ml_linear_predict(model:ML_Linear, row:a:f):f!e

What a fitted line says about one row.

ml_tree 3 functions
ml_tree_explain(model:ML_Tree, feature_names:a:s):s!e

Writes a tree out as the rules it actually applies - the reason to reach for a tree over something more accurate. Pass an empty array to see the columns numbered.

ml_tree_fit(features:a:a:f, labels:a:i, max_depth:i):ML_Tree!e

Fits a decision tree by repeatedly splitting on whichever column separates the classes best. The maximum depth is what stands between a useful model and one that has memorised the training set - three to five is a sensible start.

ml_tree_predict(model:ML_Tree, row:a:f):i!e

What a fitted tree says about one row.

money 10 functions
money_allocate(cents:i, weights:a:i):a:i!e

Divides an amount in proportion to the given weights, with every cent accounted for and the earliest weights taking any leftover.

money_format(cents:i, symbol:s):s

Writes an amount of cents with a symbol, thousands separators and two decimal places.

money_from_dollars(dollars:f):i

Returns the number of cents an amount of dollars comes to, rounded to the nearest cent. The one place a float is involved.

money_loan_payment(principal_cents:i, annual_rate_percent:f, months:i):i!e

The fixed monthly payment that clears a loan, in cents, with the quoted rate compounded monthly (the United States convention, and what most loan calculators mean). Canadian mortgages compound twice a year, so use money_loan_payment_compounded with 2 for those. The rate is the yearly percentage as people quote it: 6.0 means six percent.

money_loan_payment_compounded(principal_cents:i, annual_rate_percent:f, months:i, compounds_per_year:i):i!e

The same fixed monthly payment when the quoted rate compounds some other number of times a year. Canada compounds mortgages twice a year by law, so compounds_per_year is 2 there. 12 matches money_loan_payment exactly, and 365 approximates a daily-compounding lender.

money_parse(text:s):i!e

Reads what a person typed as a number of cents, accepting a currency symbol, thousands separators, a minus sign or brackets. More precision than a cent is an error.

money_percent_of(cents:i, rate:f):i

Returns a percentage of an amount, rounded to the nearest cent. The rate is a percentage, so 5.0 is five percent.

money_split(cents:i, ways:i):a:i!e

Splits an amount as evenly as it can be, handing the leftover cents out one each from the start so the shares add back up to the whole.

money_times(cents:i, count:i):i!e

Returns an amount multiplied by a count, which is what a line item comes to. Errors if the total is larger than can be counted.

money_to_dollars(cents:i):f

Returns an amount of cents as a number of dollars, for handing to something that insists on one.

net 17 functions
net_dns_lookup(hostname:s):a:s!e

Returns every address a hostname resolves to, in the order the resolver gave them.

net_dns_mx(domain:s):a:s!e

Returns the mail hosts a domain names, in the order mail should be tried. This is the check worth doing on a typed email address that validate_email cannot make: whether anything accepts mail there at all. A domain naming none is an error.

net_dns_reverse(ip:s):a:s!e

Returns the names an address points back at, for turning a log line into something a person reads. Most addresses have no reverse name, and that is an error rather than an empty list.

net_dns_txt(name:s):a:s!e

Returns the text records on a name, one string per record - an SPF or DMARC policy, a verification token a service asked to have put there, a signing key. A record written in several quoted pieces comes back joined.

net_ip_from_int(value:i):s!e

The integer back to its dotted v4 form.

net_ip_in_cidr(ip:s, cidr:s):b!e

Whether an address sits inside a CIDR range like `10.0.0.0/8` - how an allowlist is checked. An address of the other family is outside the range, not an error.

net_ip_is_loopback(ip:s):b!e

Whether an address points back at the machine itself.

net_ip_is_private(ip:s):b!e

Whether an address is private - RFC 1918 space for v4, unique-local for v6.

net_ip_to_int(ip:s):i!e

A v4 address as the integer it is - what log databases store and range comparisons sort. A v6 address does not fit and says so.

net_ip_version(ip:s):i!e

4 or 6, after checking the text really is an address.

net_tcp_is_open(host:s, port:i, timeout_milliseconds:i):b!e

Returns whether something is listening on a port. A refused connection and an unreachable host are both false.

net_tcp_request(host:s, port:i, text:s, timeout_milliseconds:i):s!e

Opens a TCP connection, sends the text exactly as given, and returns everything sent back until the other end closes or the timeout runs out.

net_tcp_serve(host:s, port:i):v!e

Accepts TCP connections and speaks a line-at-a-time protocol: each line a client sends is answered by the program's handle_line(line:s):s function, and an empty reply sends nothing back. Blocks forever, so it runs in a c block beside the rest of the program. Bind `127.0.0.1` to stay behind a reverse proxy, `0.0.0.0` to face the world.

net_tls_cert_days_left(hostname:s, port:i, timeout_milliseconds:i):i!e

Returns how many whole days are left before the certificate a server presents stops being valid - the number a scheduled check compares against. An expired or untrusted certificate fails the handshake and the error says which.

net_tls_cert_expiry(hostname:s, port:i, timeout_milliseconds:i):i!e

Returns when the certificate a server presents stops being valid, as a Unix timestamp to compare with time_now. Read from a real handshake, so it is what the server serves today rather than what a renewal script believes it installed.

net_udp_request(host:s, port:i, text:s, timeout_milliseconds:i):s!e

Sends one UDP datagram and waits for one back. A timeout means no answer came, not that the host is down.

net_udp_serve(host:s, port:i):v!e

Answers UDP datagrams: each one is passed as text to the program's handle_packet(packet:s):s function, and a non-empty reply goes back to whoever asked. Blocks forever, so it runs in a c block beside the rest of the program.

panic 2 functions
panic(message:s):v

Prints the message to stderr and aborts the program immediately. Never returns.

todo(message:s):v

Marks unfinished code: prints the message to stderr and aborts. Never returns.

path 19 functions
path_absolute(path:s):s!e

Returns the path resolved against the directory the program is running in. Works for a file that does not exist yet.

path_basename(path:s):s

Returns the final component of the path (the file or directory name).

path_common_prefix(paths:a:s):s

Returns the longest directory prefix the paths share, whole segments only, and an empty string when nothing is shared.

path_depth(path:s):i

Returns how many segments the path has below its root or its start, counted after normalizing, so a/b/c.txt is 3 and / is 0.

path_dirname(path:s):s

Returns the path without its final component.

path_exists(path:s):b

Returns true if a file or directory exists at the path.

path_extension(path:s):s!e

Returns the file extension without the dot. Errors if there is none.

path_is_absolute(path:s):b

Returns true if the path is absolute.

path_is_hidden(path:s):b

Returns true when the file name starts with a dot, judged on the final component only.

path_join(base:s, path:s):s

Joins two path segments with the platform separator.

path_matches_glob(pattern:s, path:s):b

Whether a path matches a shell glob pattern, where * stays inside one segment, ** crosses segments, ? is one character and [abc] is one of those listed.

path_normalize(path:s):s

Normalizes a path by resolving . and .. components.

path_relative_to(path:s, base:s):s!e

Returns the path written from the base directory instead. Errors if the path is not inside the base.

path_sanitize_filename(name:s):s

Makes untrusted text safe as a single file name: separators, dot-dot, control characters, characters Windows refuses and leading dots become underscores, and empty input gives file.

path_segments(path:s):a:s

Returns the pieces of the path between separators, with the empties a doubled or trailing separator would make dropped.

path_stem(path:s):s

Returns the file name with its last extension removed, so report.tar.gz gives report.tar.

path_with_extension(path:s, extension:s):s

Returns the same path carrying a different extension. An empty extension removes it.

path_with_stem(path:s, stem:s):s!e

Returns the path with a different file name but the same directory and extension. Errors on an empty stem or a path with no file name.

path_within(base:s, candidate:s):b

Returns true when the candidate stays inside the base once both are normalized, so a .. that climbs out is caught. Pure string work, no filesystem access.

pdf 2 functions
pdf_from_text(path:s, title:s, body:s):v!e

Writes a paginated A4 PDF of a title and plain text body, wrapping lines and flowing onto as many pages as needed. For tables and letterheads, use real typesetting instead.

pdf_text(path:s):s!e

The text of a PDF, in reading order as far as the file allows. A scanned PDF is photographs and gives back nothing - that needs OCR, which this is not.

print 5 functions
print(message:T):v

Prints a value to stdout followed by a newline.

print_clear_screen():v

Clears the terminal screen and moves the cursor to the top left.

print_debug(value:T):v

Prints a value in expanded debug format, useful for structs and arrays.

print_error(message:T):v

Prints a value to stderr followed by a newline, so a note about the run does not land in the answer the run produced. For anything with a level or a timestamp, use the log module instead.

print_no_newline(message:T):v

Prints a value to stdout without a trailing newline.

process 15 functions
process_close_stdin(process:PROCESS_Handle):v!e

Closes the process's stdin - the end-of-input many programs wait for before finishing.

process_default_options():PROCESS_Options

The default options for running a command: here, with nothing added, no input, and no time limit. Nail has no default field values, so this saves spelling out every field of PROCESS_Options.

process_exit(code:i):v

Terminates the program immediately with the given exit code. Never returns.

process_is_running(process:PROCESS_Handle):b!e

Whether the process is still going.

process_kill(process:PROCESS_Handle):v!e

Stops the process now and forgets its handle.

process_next_line(process:PROCESS_Handle):s!e

The next line the process printed, stdout and stderr together in arrival order. Waits for one if none is ready. An error means the output is over. The shape of a tail loop is: ask for lines with safe(), stop on the error.

process_open_browser(url:s):v!e

Opens a URL in the person's browser through the desktop's own opener. For local tools that want to show the page they just made.

process_run(command:s, arguments:a:s):s!e

Runs an external command and returns its stdout. Errors if the command fails.

process_run_result(command:s, arguments:a:s):PROCESS_Result!e

Runs a command and returns everything about how it went: both its output streams and its exit code. A command that fails is not an error here - the exit code is the answer. Errors only when the command could not be started at all.

process_run_with(command:s, arguments:a:s, options:PROCESS_Options):PROCESS_Result!e

Runs a command in another directory, with extra environment variables, with text on its standard input, or with a time limit - whichever of those the options set. A command that runs out of time is killed.

process_spawn(command:s, arguments:a:s):PROCESS_Handle!e

Starts a program and keeps it running - what process_run cannot do, because it collects everything at the end. Output streams out through process_next_line. process_wait collects the exit code.

process_wait(process:PROCESS_Handle):i!e

Waits for the process to end and returns its exit code. Read the lines you want first - waiting forgets the handle, and any unread output with it.

process_wait_for_interrupt():v!e

Waits until the program is asked to stop - Ctrl-C, or the TERM signal a service manager sends - and returns when it is. Put it after starting everything, and shut down cleanly afterwards instead of being killed mid-request.

process_which(name:s):s!e

Returns where a command would be found on PATH, the way which answers it. Errors if there is no such program. What to check before offering a feature that shells out.

process_write_stdin(process:PROCESS_Handle, text:s):v!e

Writes text to the process's stdin, exactly as given - add a newline yourself when the program reads lines.

rand 13 functions
rand_bool():b

Returns true or false with even odds.

rand_chance(probability:f):b!e

Returns true with the given probability from 0.0 to 1.0. Errors on anything outside that.

rand_float():f

Returns a random fraction from 0.0 up to but not including 1.0.

rand_float_range(min:f, max:f):f!e

Returns a random fraction from min up to but not including max. Errors if min is above max.

rand_int(min:i, max:i):i!e

Returns a random whole number from min to max, both ends included. Errors if min is above max.

rand_normal(mean:f, stddev:f):f

Returns a value from a normal distribution with the given mean and standard deviation.

rand_pick(items:a:T):T!e

Returns one element of the array, chosen evenly. Errors if the array is empty.

rand_sample(items:a:T, count:i):a:T!e

Returns the given number of elements drawn without replacement, in random order. Errors if the array is smaller than that.

rand_seeded_float(seed:i):f

Returns the same fraction every time for a given seed, from 0.0 up to 1.0.

rand_seeded_int(seed:i, min:i, max:i):i!e

Returns the same whole number every time for a given seed, from min to max inclusive. Use it when a random result has to be reproducible.

rand_seeded_normal(seed:i, mean:f, stddev:f):f

Returns the same normally distributed value every time for a given seed, with the given mean and standard deviation.

rand_seeded_shuffle(seed:i, items:a:T):a:T

Returns the array in the same shuffled order every time for a given seed.

rand_weighted_pick(options:a:T, weights:a:f):T!e

Returns one element of the array, chosen with probability proportional to its weight. A zero weight is never chosen. Errors if the arrays differ in length, a weight is negative, or no weight is positive.

regex 11 functions
regex_capture_named(pattern:s, text:s, name:s):s!e

Returns one named capture group of the first match, for patterns written with (?<name>...).

regex_captures(pattern:s, text:s):a:s!e

Returns the capture groups of the first match with the whole match first. Errors if the pattern is invalid or nothing matches.

regex_count(pattern:s, text:s):i!e

Returns how many times the pattern matches, which may be zero.

regex_escape(text:s):s

Escapes every regex character in the text so it can be put inside a pattern and match only itself.

regex_find(pattern:s, text:s):s!e

Returns the first regex match in the text. Errors if the pattern is invalid or nothing matches.

regex_find_all(pattern:s, text:s):a:s!e

Returns all regex matches in the text. Errors if the pattern is invalid or nothing matches.

regex_is_valid(pattern:s):b

Returns true if the text is a usable regex pattern, for checking a search a visitor typed before running it.

regex_match(pattern:s, text:s):b!e

Returns true if the regex pattern matches anywhere in the text. Errors on an invalid pattern.

regex_replace(pattern:s, text:s, replacement:s):s!e

Replaces all regex matches in the text with the replacement. Errors on an invalid pattern.

regex_replace_first(pattern:s, text:s, replacement:s):s!e

Replaces only the first regex match, where regex_replace replaces every one.

regex_split(pattern:s, text:s):a:s!e

Splits the text by a regex pattern. Errors on an invalid pattern.

sched 2 functions
sched_every(name:s, seconds:i):v!e

Calls the program's handle_job(name:s):v function with the given name every so many seconds, forever. The wait is between finishes, not starts, so slow work never overlaps itself. Blocks forever, so it runs in a c block beside the rest of the program.

sched_run(jobs:a:SCHED_Job):v!e

Runs jobs on their cron schedules, forever - each due moment calls the program's handle_job(name:s):v function with the job's name. Jobs run one at a time in this loop, so they never overlap. Blocks forever, so it runs in a c block beside the server. The error case is a cron expression that does not parse.

semver 14 functions
semver_bump_major(version:s):s!e

Returns the next major version, resetting the numbers below it.

semver_bump_minor(version:s):s!e

Returns the next minor version, resetting the patch number.

semver_bump_patch(version:s):s!e

Returns the next patch version.

semver_compare(first:s, second:s):i!e

Returns -1 if the first version is older, 0 if they are the same, and 1 if the first is newer.

semver_is_newer(first:s, second:s):b!e

Returns true if the first version is newer than the second.

semver_is_older(first:s, second:s):b!e

Returns true if the first version is older than the second.

semver_major(version:s):i!e

Returns the major number of a version.

semver_minor(version:s):i!e

Returns the minor number of a version.

semver_newest(versions:a:s):s!e

Returns the newest of the versions, or an error if there are none.

semver_patch(version:s):i!e

Returns the patch number of a version.

semver_prerelease(version:s):s!e

Returns the prerelease part without its hyphen, or the empty string if there is none.

semver_satisfies(version:s, requirement:s):b!e

Returns true if the version meets the requirement, which may be exact, a comparison, a caret or tilde range, a star, or several of those separated by commas.

semver_sort(versions:a:s):a:s!e

Returns the versions sorted oldest first.

semver_valid(version:s):b

Returns true if the text is a version number this module can read.

stats 50 functions
stats_ab_test(conversions_a:i, visitors_a:i, conversions_b:i, visitors_b:i):f!e

Returns the p-value that variant B converts differently from variant A, the two-proportion z-test in experiment words, with 0.05 the conventional bar for calling a winner. Errors unless each arm has at least one visitor and the conversions fit their visitors.

stats_binomial_cdf(successes:i, trials:i, probability:f):f!e

Returns the probability of at most that many successes in the trials, the pmf summed in log space. Errors unless the successes run from 0 to the trials and the probability from 0.0 to 1.0.

stats_binomial_pmf(successes:i, trials:i, probability:f):f!e

Returns the probability of exactly that many successes in the trials, computed in log space so a thousand trials cannot overflow. Errors unless the successes run from 0 to the trials and the probability from 0.0 to 1.0.

stats_chi_square_test(observed:a:f, expected:a:f):f!e

Returns the goodness-of-fit p-value comparing observed counts against expected ones, the chance of a mismatch this large if the expectation were right. Errors on mismatched lengths, fewer than two cells, or an expected count that is not positive.

stats_confidence_interval_95(values:a:f):f!e

Returns the plus-or-minus half width of the 95 percent t-interval for the mean, the distance the true mean sits within 95 percent of the time. Errors on fewer than two values.

stats_correlation(first:a:f, second:a:f):f!e

Returns how closely two columns move together, from -1.0 to 1.0. Errors on mismatched lengths or a column that never changes.

stats_covariance(first:a:f, second:a:f):f!e

Returns the sample covariance, positive when two columns rise together, in the product of their units. Errors on mismatched lengths or fewer than two pairs.

stats_cumulative_sum(values:a:f):a:f

Returns the running total after each value. An empty array stays empty.

stats_cv(values:a:f):f!e

Returns the standard deviation as a share of the mean, comparable across different units. Errors on fewer than two values or a zero mean.

stats_differences(values:a:f):a:f

Returns the step from each value to the next, one shorter than the input. An empty array stays empty.

stats_ewma(values:a:f, alpha:f):a:f!e

Returns the exponentially weighted moving average - the factor is above 0.0 and at most 1.0, smaller meaning smoother. Errors on an empty array or a factor outside that range.

stats_geometric_mean(values:a:f):f!e

Returns the n-th root of the product, the right average for growth rates and ratios. Errors unless every value is positive.

stats_harmonic_mean(values:a:f):f!e

Returns the reciprocal of the mean of reciprocals, the right average for rates like speeds. Errors unless every value is positive.

stats_histogram(values:a:f, bins:i):a:i!e

Returns counts per equal-width bin from the smallest value to the largest, the largest landing in the last bin. Errors on an empty array or fewer than one bin.

stats_iqr(values:a:f):f!e

Returns the width of the middle half of the data, a spread one outlier cannot inflate. Errors on an empty array.

stats_kurtosis(values:a:f):f!e

Returns excess sample kurtosis, positive for heavy tails and negative for flat-topped data. Errors on fewer than four values or flat data.

stats_mad(values:a:f):f!e

Returns the median distance from the median, the most outlier-resistant spread measure. Errors on an empty array.

stats_mean(values:a:f):f!e

Returns the average of the values. Errors on an empty array.

stats_median(values:a:f):f!e

Returns the middle value once sorted, which one outlier cannot move. Errors on an empty array.

stats_midrange(values:a:f):f!e

Returns the midpoint between the smallest and largest value. Errors on an empty array.

stats_min_detectable_effect(visitors_per_arm:i, baseline_rate:f):f!e

Returns the smallest absolute rate change an A/B test with that many visitors per arm can reliably detect, at 80 percent power and two-sided 5 percent significance. Errors unless each arm has at least one visitor and the baseline rate sits strictly between 0 and 1.

stats_mode(values:a:f):f!e

Returns the value that appears most often, the smallest one when several tie. Errors on an empty array.

stats_moving_average(values:a:f, window:i):a:f!e

Returns the mean of each window-sized run of neighbours, smoothing a noisy series. Errors unless the window fits inside the array.

stats_normal_cdf(value:f, mean:f, stddev:f):f!e

Returns the probability that a normal draw lands at or below the value. Errors unless the standard deviation is positive.

stats_normal_inverse(probability:f, mean:f, stddev:f):f!e

Returns the value below which the given share of a normal distribution falls, the inverse of stats_normal_cdf. Errors unless the probability is strictly between 0 and 1 and the standard deviation is positive.

stats_normal_pdf(value:f, mean:f, stddev:f):f!e

Returns the height of the normal bell curve at the value, a density rather than a probability. Errors unless the standard deviation is positive.

stats_normalize(values:a:f):a:f!e

Returns each value scaled linearly onto 0.0..1.0, smallest to largest. Errors on an empty array or flat data.

stats_outliers(values:a:f):a:f

Returns the values beyond the 1.5-IQR boxplot fences, in their original order. Fewer than four values report none.

stats_percent_change(values:a:f):a:f!e

Returns the percent change from each value to the next, one shorter than the input. Errors when a step starts from zero.

stats_percentile(values:a:f, share:f):f!e

Returns the value below which the given share of the data falls, written from 0.0 to 1.0.

stats_percentile_rank(values:a:f, target:f):f!e

Returns the share of values at or below the target, from 0.0 to 100.0 - the inverse question to stats_percentile. Errors on an empty array.

stats_poisson_cdf(events:i, rate:f):f!e

Returns the probability of at most that many events arriving at the given average rate, the pmf summed in log space. Errors unless the rate is positive and the count nonnegative.

stats_poisson_pmf(events:i, rate:f):f!e

Returns the probability of exactly that many events arriving at the given average rate, computed in log space so large counts cannot overflow. Errors unless the rate is positive and the count nonnegative.

stats_proportion_test(successes_a:i, total_a:i, successes_b:i, total_b:i):f!e

Returns the two-sided p-value of a pooled two-proportion z-test, the chance of a gap this large between two success rates if they truly matched. Errors unless each total is at least one, each success count fits its total, and the outcomes vary at all.

stats_pstddev(values:a:f):f!e

Returns the population standard deviation, in the units of the data. Errors on an empty array.

stats_pvariance(values:a:f):f!e

Returns the population variance, dividing by n, for when the values are the whole population. Errors on an empty array.

stats_quartiles(values:a:f):a:f!e

Returns the 25th, 50th and 75th percentiles as a three-value array, the box of a boxplot in one call. Errors on an empty array.

stats_range(values:a:f):f!e

Returns the distance from the smallest value to the largest. Errors on an empty array.

stats_rank(values:a:f):a:f

Returns the 1-based rank of each value in the original order, tied values sharing the average of their positions.

stats_rms(values:a:f):f!e

Returns the root mean square, the natural magnitude for values that swing through zero. Errors on an empty array.

stats_sample_size_for_proportion(margin_of_error:f, confidence:f):i!e

Returns how many people to survey so an estimated proportion lands within the margin of error at the given confidence, the classic poll planning number, rounded up. Errors unless both the margin and the confidence are strictly between 0 and 1.

stats_sem(values:a:f):f!e

Returns the standard error of the mean, how far the sample mean likely sits from the true one. Errors on fewer than two values.

stats_skewness(values:a:f):f!e

Returns adjusted sample skewness, positive when the long tail points right. Errors on fewer than three values or flat data.

stats_spearman(first:a:f, second:a:f):f!e

Returns rank correlation, which sees any steadily rising or falling relationship, straight line or not. Errors on mismatched lengths, fewer than two pairs, or a flat column.

stats_stddev(values:a:f):f!e

Returns the sample standard deviation, in the units of the data. Errors on fewer than two values.

stats_t_test(first:a:f, second:a:f):f!e

Returns the two-sided p-value of a Welch two-sample t-test, the chance of a gap this large between the means if the groups truly matched. Errors unless each sample has at least two values and at least one sample has spread.

stats_trimmed_mean(values:a:f, trim_share:f):f!e

Returns the mean after dropping the given share of values from each end, blunting outliers. The share runs from 0.0 up to but not including 0.5.

stats_variance(values:a:f):f!e

Returns the sample variance. Errors on fewer than two values.

stats_weighted_mean(values:a:f, weights:a:f):f!e

Returns the mean with each value counted by its weight. Errors on mismatched lengths or weights that do not sum to a positive total.

stats_zscores(values:a:f):a:f!e

Returns each value as its distance from the mean in standard deviations. Errors on fewer than two values or flat data.

stdlib 2 functions
stdlib_functions():a:STDLIB_Function

Every function the standard library provides, as data: name, module, signature, description and example. Sorted by module, then by name. The list comes from the same registry the type checker uses, so it is exactly what this compiler can call.

stdlib_modules():a:s

The standard library's namespaces, spelled the way calls spell them (db, string, net), in the order stdlib_functions lists their functions.

string 110 functions
string_after(input:s, marker:s):s!e

Returns everything after the first occurrence of the marker. Errors if the marker is not there.

string_before(input:s, marker:s):s!e

Returns everything before the first occurrence of the marker. Errors if the marker is not there.

string_best_match(query:s, candidates:a:s):s!e

Returns the candidate with the highest trigram similarity to the query, ties going to the earlier one. Where string_closest picks by edit distance, this rewards shared fragments. Errors if there are no candidates.

string_between(input:s, start:s, end:s):s!e

Returns the text between the first start marker and the next end marker after it. The error names which marker is missing.

string_capitalize(input:s):s

Uppercases the first character of the string.

string_center(input:s, width:i, pad:s):s

Centers the string in a field of the given width, padding both sides. When uneven, the extra goes on the right.

string_char_at(input:s, index:i):s!e

Returns the single character at the index as a string, or an error if the index is out of bounds.

string_char_code(input:s, index:i):i!e

Returns the Unicode code point of the character at the index, so A is 65. Errors if the index is out of bounds.

string_chars(input:s):a:s

Splits a string into an array of single-character strings.

string_closest(input:s, candidates:a:s):s!e

Returns the candidate most like the input, for answering a typo with a suggestion, or an error if there are no candidates.

string_common_prefix(strings:a:s):s

Returns the beginning that all the strings share, or the empty string if they share none.

string_common_suffix(strings:a:s):s

Returns the ending that all the strings share, or the empty string if they share none - the counterpart of string_common_prefix.

string_compare_natural(first:s, second:s):i

Compares two pieces of text the way a person reads names with numbers in them, reading a run of digits as the number it spells. Returns -1 when the first comes earlier, 1 when it comes later, 0 when they are the same text. array_sort_natural sorts a whole array this way.

string_concat(strings:a:s):s

Concatenates an array of strings into a single string.

string_contains(input:s, pattern:s):b

Returns true if the string contains the given substring.

string_contains_ignore_case(input:s, pattern:s):b

Returns true if the string holds the given substring, ignoring capitals.

string_cosine_words(first:s, second:s):f

Returns the cosine similarity of the lowercase word-count vectors, the duplicate-aware cousin of string_jaccard_words.

string_count(input:s, substring:s):i

Counts non-overlapping occurrences of a substring.

string_dedent(input:s):s

Removes the leading whitespace every non-blank line shares, keeping the relative shape.

string_delete_whitespace(input:s):s

Removes every whitespace character, line breaks and tabs included.

string_digits_only(input:s):s

Keeps only the digit characters 0-9, in order - what a phone number field needs before dialing.

string_ends_with(input:s, suffix:s):b

Returns true if the string ends with the given suffix.

string_ends_with_ignore_case(input:s, suffix:s):b

Returns true if the string ends with the given suffix, ignoring capitals.

string_ensure_prefix(input:s, prefix:s):s

Adds the prefix only when the string does not already start with it.

string_ensure_suffix(input:s, suffix:s):s

Adds the suffix only when the string does not already end with it.

string_equals_ignore_case(first:s, second:s):b

Returns true if two strings are the same when the difference between capital and small letters does not count. The comparison for things people type: header names, commands, answers at a prompt.

string_escape_html(text:s):s

Escapes &, <, >, " and ' so text a visitor supplied can be put in a page without becoming markup.

string_first_line(input:s):s

Returns the first line of the text, without its line break.

string_from(value:T):s

Converts any value (int, float, bool, struct, etc.) to its string representation.

string_from_array_bool(array:a:b):s

Converts an array of booleans to a string like [true, false].

string_from_array_f64(array:a:f):s

Converts an array of floats to a string like [1.5, 2.5].

string_from_array_i64(array:a:i):s

Converts an array of integers to a string like [1, 2, 3].

string_from_array_string(array:a:s):s

Converts an array of strings to a string like [a, b, c].

string_from_char_code(code:i):s!e

Returns the one-character string for a Unicode code point. Errors on a number that is not one.

string_grapheme_length(input:s):i

How many characters a person sees - what a length limit on human text should count, where string_length overcounts emoji and accents.

string_graphemes(input:s):a:s

The characters a person sees, one string each. An emoji with skin tone or a flag is one grapheme even though it is several code points.

string_hamming_distance(first:s, second:s):i!e

Counts the positions where two equal-length strings differ. Errors when the lengths differ.

string_has_emoji(text:s):b

Returns true when the text holds at least one emoji character.

string_indent(input:s, prefix:s):s

Puts the prefix in front of every non-blank line.

string_index_of(input:s, substring:s):i!e

Returns the index of the first occurrence of a substring, or an error if not found.

string_initials(input:s):s

Returns the uppercased first letter of each word - `Ada Lovelace` gives `AL`.

string_is_alphabetic(input:s):b

Returns true if the string is non-empty and contains only alphabetic characters.

string_is_alphanumeric(input:s):b

Returns true if the string is non-empty and contains only letters and digits.

string_is_blank(input:s):b

Returns true if the string is empty or holds only whitespace - the check string_is_empty lets ` ` slip past.

string_is_digits_only(input:s):b

Returns true if the string is non-empty and contains only digits 0-9.

string_is_empty(input:s):b

Returns true if the string has no characters.

string_is_lowercase(input:s):b

Returns true if the string has letters and none of them is uppercase.

string_is_numeric(input:s):b

Returns true if the string parses as a number (including floats and negatives).

string_is_uppercase(input:s):b

Returns true if the string has letters and none of them is lowercase.

string_jaccard_words(first:s, second:s):f

Returns the Jaccard similarity of the lowercase word sets, from 0.0 to 1.0. Repeats do not count, which is what string_cosine_words is for.

string_join(array:a:s, separator:s):s

Joins an array of strings with a separator between elements.

string_last_index_of(input:s, substring:s):i!e

Returns the index of the last occurrence of a substring, or an error if not found.

string_last_line(input:s):s

Returns the last line of the text, without its line break. A trailing newline does not count as an extra line.

string_length(input:s):i

Returns the number of characters in the string.

string_letters_only(input:s):s

Keeps only the letters, in order, dropping digits, punctuation and whitespace.

string_levenshtein(first:s, second:s):i

Returns how many single-character edits turn one string into the other.

string_mask(input:s, visible_tail:i, mask_character:s):s

Replaces all but the last few characters, for showing which secret is in use without printing it.

string_minify(input:s):s

Removes all whitespace outside of quoted strings (useful for minifying JSON).

string_normalize_nfc(input:s):s

Unicode NFC normalization, the composed form. Two spellings of `café` compare equal after both pass through here - normalize before comparing or storing anything people typed.

string_normalize_nfkc(input:s):s

Unicode NFKC normalization, the compatibility form. Fullwidth letters, ligatures and font tricks collapse to plain equivalents - what searching and usernames want.

string_normalize_whitespace(input:s):s

Collapses every run of whitespace to one space and trims the ends.

string_pad_end(input:s, target_length:i, pad_str:s):s

Pads the end of the string with pad_str until it reaches target_length.

string_pad_start(input:s, target_length:i, pad_str:s):s

Pads the start of the string with pad_str until it reaches target_length.

string_parse_number(text:s):f!e

Reads the number out of text a person wrote or a spreadsheet exported: a currency symbol, spaces, digit grouping and a trailing percent sign are all thrown away, and brackets around the whole amount mean it is negative. Text with a letter in it is an error rather than a guess.

string_reading_time_minutes(input:s):i

Estimates reading time in whole minutes at 200 words per minute, rounded up - at least 1 for any words, 0 for none.

string_remove_accents(input:s):s

The text with its accents dropped: `café` becomes `cafe`. What slugs and diacritic-blind search ask for.

string_repeat(input:s, count:i):s

Repeats the string count times.

string_replace(input:s, from:s, to:s):s

Replaces every occurrence of a substring with another string.

string_replace_first(input:s, from:s, to:s):s

Replaces only the first occurrence of a substring with another string.

string_reverse(input:s):s

Reverses the order of characters in the string.

string_rot13(input:s):s

Rotates each ASCII letter 13 places through the alphabet - the classic reversible scramble. Applying it twice gives the text back.

string_shell_quote(word:s):s

Writes one word so a shell reads it as exactly this text, for a command line built out of values. Text a shell would already read whole comes back untouched.

string_shell_split(line:s):a:s!e

Splits a command line into the words a shell would pass to a program, so process_run can be handed something a person typed. Quotes group words and a backslash protects the next character. A quote that is never closed is an error.

string_similarity(first:s, second:s):f

Returns how alike two strings are, from 0.0 for nothing in common to 1.0 for identical.

string_slice(input:s, start:i, end:i):s!e

Returns the substring from start (inclusive) to end (exclusive), or an error if out of bounds.

string_slugify(input:s):s

Turns a title into a URL part: lowercase words joined by single hyphens.

string_soundex(name:s):s

Returns the classic American Soundex code, a letter plus three digits, so names that sound alike code alike. Letterless input gives an empty string.

string_sounds_like(first:s, second:s):b

Returns true when two names share the same non-empty Soundex code, so `Robert` matches `Rupert`.

string_split(input:s, delimiter:s):a:s

Splits a string into an array of substrings by a delimiter.

string_split_last(input:s, separator:s):a:s!e

Splits at the last separator and returns the two halves. Errors if the separator is not there.

string_split_lines(input:s):a:s

Splits a string into an array of lines.

string_split_once(input:s, separator:s):a:s!e

Splits at the first separator only and returns the two halves, so a value containing the separator stays whole. Errors if the separator is not there.

string_split_whitespace(input:s):a:s

Splits a string on runs of whitespace, dropping empty entries.

string_squeeze(input:s):s

Collapses every run of the same repeated character to one, so `aaabbb` becomes `ab`.

string_starts_with(input:s, prefix:s):b

Returns true if the string starts with the given prefix.

string_starts_with_ignore_case(input:s, prefix:s):b

Returns true if the string starts with the given prefix, ignoring capitals.

string_strip_emoji(text:s):s

Removes emoji from the text, keeping ordinary letters, accents and CJK. Flag sequences and keycaps reduce to their leftover parts.

string_strip_prefix(input:s, prefix:s):s

Removes the prefix if the string starts with it, and returns the string unchanged otherwise.

string_strip_suffix(input:s, suffix:s):s

Removes the suffix if the string ends with it, and returns the string unchanged otherwise.

string_substring(input:s, start:i, end:i):s!e

Returns the substring from start (inclusive) to end (exclusive), or an error if out of bounds.

string_swap_case(input:s):s

Flips the case of every letter: uppercase becomes lowercase and lowercase becomes uppercase.

string_to_camel_case(input:s):s

Converts to camelCase, the spelling JSON keys and JavaScript APIs use.

string_to_kebab_case(input:s):s

Converts a string to kebab-case.

string_to_lowercase(input:s):s

Converts all characters to lowercase.

string_to_pascal_case(input:s):s

Converts to PascalCase, the spelling type names use.

string_to_sentence_case(input:s):s

Capitalizes only the first letter of the string, lowercasing the rest.

string_to_snake_case(input:s):s

Converts a string to snake_case.

string_to_title_case(input:s):s

Capitalizes the first letter of every word.

string_to_uppercase(input:s):s

Converts all characters to uppercase.

string_trigram_similarity(first:s, second:s):f

Returns how alike two strings look by comparing their sets of lowercased three-character windows. Forgiving of swapped and missing letters where string_similarity counts edits in order.

string_trim(input:s):s

Removes leading and trailing whitespace.

string_trim_chars(input:s, characters:s):s

Removes any of the given characters from both ends, the way string_trim removes whitespace.

string_trim_end(input:s):s

Removes trailing whitespace.

string_trim_end_chars(input:s, characters:s):s

Removes any of the given characters from the end only.

string_trim_start(input:s):s

Removes leading whitespace.

string_trim_start_chars(input:s, characters:s):s

Removes any of the given characters from the start only.

string_truncate(input:s, max_length:i, ellipsis:s):s

Cuts text to a maximum length, counting the ellipsis as part of that length.

string_unescape_html(text:s):s

Turns HTML entities such as &amp;lt; and &amp;#39; back into the characters they stood for.

string_word_count(input:s):i

Returns how many whitespace-separated words the text holds.

string_word_wrap(input:s, width:i):s

Breaks text into lines no wider than the given number of characters, splitting between words.

sys 9 functions
sys_cpu_usage_percent():f

CPU use across all cores as a percentage. Sampling takes a moment - the number is measured over a short interval, not read from a counter.

sys_disk_free_bytes(path:s):i!e

Bytes still free on the disk holding a path - the number that stops a full disk from being a surprise.

sys_disk_total_bytes(path:s):i!e

The whole size of the disk holding a path, in bytes.

sys_load_average():f

The one-minute load average - how many cores' worth of work is waiting. Above env_cpu_count means the machine is behind.

sys_memory_available_bytes():i

Memory still available to programs, in bytes.

sys_memory_total_bytes():i

Physical memory in bytes. format_bytes turns it into something readable.

sys_process_cpu_percent():f!e

How much CPU this very program is using, as a percentage of one core - 200.0 means two cores' worth. Sampled over a short interval.

sys_process_memory_bytes():i!e

How much memory this very program is using, in bytes - the number to put on a health endpoint and watch for leaks.

sys_uptime_seconds():i

Seconds since the machine booted.

template 5 functions
template_has(template:s, name:s):b!e

Returns whether the template mentions the named placeholder, in a value tag or as the name a conditional asks about. A template that does not mention it is false, and a template that cannot be read is an error.

template_names_used(template:s):a:s!e

Returns the names a template asks for, so a program can check it holds them before rendering.

template_render(template:s, values:h<s,s>):s!e

Fills the values into the template, escaping each one for HTML. {{name}} is escaped, {{{name}}} is raw, {{#if name}}...{{else}}...{{/if}} and {{#unless name}}...{{/unless}} choose a part, and {{! }} is a comment. A name the values do not have is an error.

template_render_or(template:s, values:h<s,s>, fallback:s):s!e

Fills the values into the template like template_render, except that a name the values do not have becomes the fallback text instead of an error. A template that cannot be read is still an error, because no fallback repairs a tag that is never closed.

template_render_rows(template:s, rows:a:h<s,s>):s!e

Renders the same template once for each set of values and joins the results, which is how a table body or a list of cards is built.

term 18 functions
term_background(text:s, color:TERM_Color):s

Returns the text on the given background colour.

term_banner(text:s, character:s):s!e

Returns the text centered in a full 80 column rule of the given character. Errors when the character is not exactly one character.

term_bold(text:s):s

Returns the text wrapped in the escape codes that make a terminal show it bold.

term_box(text:s):s

Returns the text drawn inside a Unicode box sized to its widest line, measuring coloured text by what is seen.

term_dim(text:s):s

Returns the text wrapped in the escape codes that make a terminal show it faintly.

term_display_width(text:s):i

Returns how wide the text is once printed, counting what a person sees rather than the characters in the string.

term_height():i

Returns how many rows the terminal has, or 24 when there is no terminal to ask.

term_hyperlink(text:s, url:s):s

Returns a clickable link where the terminal supports them, and the plain text where it does not.

term_inverse(text:s):s

Returns the text with foreground and background swapped, which is what a selected row looks like.

term_is_tty():b

Returns whether standard output is a terminal rather than a file or a pipe. False means do not colour and do not draw progress.

term_italic(text:s):s

Returns the text wrapped in the escape codes that make a terminal show it italic.

term_paint(text:s, color:TERM_Color):s

Returns the text in the given colour.

term_progress_bar(share:f, width:i):s!e

Returns a progress bar of the given width filled to the given share from 0.0 to 1.0.

term_strip_styles(text:s):s

Removes every escape sequence, leaving the text as it will be read. Use it before writing coloured output anywhere that is not a terminal.

term_table(headers:a:s, rows:a:a:s):s!e

Returns a plain-text table with aligned columns. Errors if a row has a different number of cells than there are headers.

term_two_columns(left:s, right:s, width:i):s!e

Returns the two texts side by side, each wrapped to half the given width with a two space gutter between them. Errors when the width is outside 20 to 400 columns.

term_underline(text:s):s

Returns the text wrapped in the escape codes that make a terminal underline it.

term_width():i

Returns how many columns the terminal has, or 80 when there is no terminal to ask.

test 24 functions
test_assert(condition:b, message:s):v

Stops the program unless the condition is true, naming the check in the failure.

test_assert_array_contains(array:a:T, item:T, message:s):v

Stops the program unless the array contains the element.

test_assert_array_empty(array:a:T, message:s):v

Stops the program unless the array is empty, reporting how many elements turned up.

test_assert_array_length(array:a:T, expected:i, message:s):v

Stops the program unless the array holds exactly that many elements.

test_assert_array_not_empty(array:a:T, message:s):v

Stops the program if the array is empty.

test_assert_between_int(actual:i, low:i, high:i, message:s):v

Stops the program unless the value is between low and high, both ends included.

test_assert_contains(haystack:s, needle:s, message:s):v

Stops the program unless the text contains the fragment.

test_assert_ends_with(text:s, suffix:s, message:s):v

Stops the program unless the text ends with the suffix.

test_assert_equal_array(actual:a:T, expected:a:T, message:s):v

Stops the program unless the two arrays hold the same elements in the same order, naming the first position that differs.

test_assert_equal_bool(actual:b, expected:b, message:s):v

Stops the program unless the two booleans are equal.

test_assert_equal_float(actual:f, expected:f, tolerance:f, message:s):v

Stops the program unless the two fractions are within the tolerance of each other. Floats are never compared exactly.

test_assert_equal_hashmap(actual:h<K,V>, expected:h<K,V>, message:s):v

Stops the program unless the two hashmaps hold the same keys with the same values, listing every difference by key in a stable order.

test_assert_equal_int(actual:i, expected:i, message:s):v

Stops the program unless the two whole numbers are equal, reporting both.

test_assert_equal_string(actual:s, expected:s, message:s):v

Stops the program unless the two strings are equal, reporting both.

test_assert_false(condition:b, message:s):v

Stops the program unless the condition is false.

test_assert_greater_float(actual:f, threshold:f, message:s):v

Stops the program unless the value is strictly greater than the threshold. Ordering needs no tolerance.

test_assert_greater_int(actual:i, threshold:i, message:s):v

Stops the program unless the value is strictly greater than the threshold.

test_assert_less_float(actual:f, threshold:f, message:s):v

Stops the program unless the value is strictly less than the threshold. Ordering needs no tolerance.

test_assert_less_int(actual:i, threshold:i, message:s):v

Stops the program unless the value is strictly less than the threshold.

test_assert_not_contains(haystack:s, needle:s, message:s):v

Stops the program if the text contains the fragment.

test_assert_not_equal_int(actual:i, unwanted:i, message:s):v

Stops the program if the two whole numbers are equal.

test_assert_not_equal_string(actual:s, unwanted:s, message:s):v

Stops the program if the two strings are equal.

test_assert_starts_with(text:s, prefix:s, message:s):v

Stops the program unless the text starts with the prefix.

test_fail(message:s):v

Fails immediately, for a branch a test must never reach. Never returns.

time 59 functions
time_add_days(timestamp:i, days:i):i

Returns the timestamp shifted by the given number of days (negative to subtract).

time_add_hours(timestamp:i, hours:i):i

Returns the timestamp shifted by the given number of hours (negative to subtract).

time_add_minutes(timestamp:i, minutes:i):i

Returns the timestamp shifted by the given number of minutes (negative to subtract).

time_add_months(timestamp:i, months:i):i!e

Returns the timestamp a number of months away, keeping the day of the month where it can - the 31st moved into a shorter month lands on that month's last day.

time_add_seconds(timestamp:i, seconds:i):i

Returns the timestamp shifted by the given number of seconds (negative to subtract).

time_add_weeks(timestamp:i, weeks:i):i

Returns the timestamp shifted by the given number of weeks (negative to subtract).

time_add_workdays(timestamp:i, workdays:i):i!e

Returns the timestamp moved by a number of working days - skipping Saturdays and Sundays - keeping the time of day. Negative goes backwards. A weekend start does not count itself: Saturday plus one workday is Monday.

time_age_years(born:i, at:i):i!e

Returns the age in whole years at a moment, counted the way a person counts it: it goes up on the birthday, not at New Year. A moment before the birth is an error.

time_ago(timestamp:i, now:i):s

Writes how long ago a moment was the way a page shows it: just now, 5 minutes ago, 3 days ago, or in 2 hours for something still to come. Both moments are given so the same inputs always read the same.

time_cron_describe(expression:s):s!e

A five-field cron expression written out in words: 0 3 * * * reads every day at 03:00, and 0 9 * * 1-5 reads at 09:00 on weekdays. An expression beyond the vocabulary gets a faithful field-by-field reading rather than an error, and only an expression whose five fields do not parse is an error.

time_cron_matches(expression:s, timestamp:i):b!e

Whether a cron expression matches a moment, to the minute.

time_cron_next(expression:s, after_timestamp:i):i!e

The next moment after the given time that a cron expression matches. A scheduler asks this, sleeps until then with time_sleep, does the work, and asks again.

time_cron_valid(expression:s):b

Whether the text is a five-field cron expression this understands, for checking a schedule from a configuration file before relying on it.

time_day(timestamp:i):i!e

Returns the day of the month of a timestamp, from 1 to 31, in UTC.

time_day_of_year(timestamp:i):i!e

Returns which day of the year a timestamp falls on, from 1 to 366, in UTC.

time_days_between(start:i, end:i):i!e

Returns the whole days between the calendar dates of two moments, signed - negative when the end is earlier. The clock is ignored: 23:00 to 01:00 the next morning is 1, because the date changed once.

time_days_in_month(year:i, month:i):i!e

Returns how many days the month has, February included. Errors on a month outside 1 to 12.

time_diff(timestamp1:i, timestamp2:i):i

Returns the absolute difference between two timestamps in seconds.

time_end_of_day(timestamp:i):i!e

Returns 23:59:59 UTC on the day the timestamp falls in - the other end of time_start_of_day.

time_end_of_month(timestamp:i):i!e

Returns 23:59:59 UTC on the last day of the month the timestamp falls in - the other end of time_start_of_month, so a whole month is the range between them.

time_end_of_week(timestamp:i):i!e

Returns 23:59:59 UTC on the Sunday of the week the timestamp falls in - the other end of time_start_of_week.

time_end_of_year(timestamp:i):i!e

Returns 23:59:59 UTC on the 31st of December of the year the timestamp falls in - the other end of time_start_of_year.

time_format(timestamp:i, format:TIME_Format):s!e

Writes a Unix timestamp out in one of the standard spellings named by TIME_Format.

time_format_custom(timestamp:i, layout:s):s!e

Writes a moment out in a layout of your own, in strftime notation: %Y-%m-%d, %H:%M, %A %d %B %Y.

time_format_duration(seconds:i):s

Writes a length of time the way a person says it: 2d 3h, 1h 5m, 45s.

time_format_in_zone(timestamp:i, zone:s, layout:s):s!e

A moment shown on the wall clock of a place, in your strftime layout. Zones are IANA names like `America/Edmonton`. Daylight saving is the zone database's problem, not yours.

time_from_parts(year:i, month:i, day:i, hour:i, minute:i, second:i):i!e

Builds a moment from the parts of a UTC date. A day that is not on the calendar is an error rather than the day it would spill into.

time_hour(timestamp:i):i!e

Returns the hour of a timestamp, from 0 to 23, in UTC.

time_is_first_of_month(timestamp:i):b!e

Whether the moment falls on the first day of its month, in UTC - the day the monthly jobs run.

time_is_leap_year(year:i):b

Whether the year has a 29th of February, by the actual rule including the century exceptions.

time_is_weekend(timestamp:i):b!e

Whether the moment falls on a Saturday or Sunday, in UTC.

time_list_zones():a:s

Every zone name the database knows, for picking lists.

time_minute(timestamp:i):i!e

Returns the minute of a timestamp, from 0 to 59, in UTC.

time_month(timestamp:i):i!e

Returns the month of a timestamp, from 1 to 12, in UTC.

time_months_between(start:i, end:i):i!e

Returns the whole calendar months between two moments, signed. A month counts only once the same day of the month has been reached: the 15th of January to the 14th of March is 1, to the 15th is 2.

time_next_weekday(timestamp:i, weekday:TIME_Weekday):i!e

Returns the next date strictly after the timestamp that falls on the given weekday, keeping the time of day. A Monday asked for the next Monday gets the one a week out.

time_now():i

Returns the current Unix timestamp in seconds.

time_now_micros():i

Returns the current Unix timestamp in microseconds, for timing short work.

time_now_millis():i

Returns the current Unix timestamp in milliseconds.

time_nth_weekday_of_month(year:i, month:i, weekday:TIME_Weekday, nth:TIME_Nth):i!e

Returns the date a rule like the third Monday in January names, at midnight UTC - how holidays, pay days and standing meetings are written down. TIME_Nth::Last is its own choice rather than a count, because how many of a weekday a month holds depends on the month. A month without that many of them is an error rather than a date in the month after.

time_parse(time_str:s, format:TIME_Format):i!e

Reads a Unix timestamp out of text written in the spelling named by TIME_Format. Anything else is an error rather than a guess.

time_parse_custom(time_str:s, layout:s):i!e

Reads a moment out of text laid out the way you say, in the same strftime notation. A layout with no time in it leaves the time at midnight.

time_parse_duration(text:s):i!e

A human duration - `90s`, `2h30m`, `1.5h`, `2 days` - as whole seconds. A bare number is already seconds. The other direction is time_format_duration.

time_parse_human(text:s, reference:i):i!e

Reads a plain-English moment relative to a reference timestamp: now, today, tomorrow, yesterday, next or last plus a weekday name, in N seconds/minutes/hours/days/weeks/months, N of those units ago, or an absolute YYYY-MM-DD date. Case and extra spaces are forgiven. Anything else is an error naming the shapes it reads.

time_parse_in_zone(text:s, layout:s, zone:s):i!e

Reads a wall-clock time as seen in a place back into a timestamp. The repeated hour when clocks fall back takes the earlier reading. The skipped hour is an error.

time_quarter(timestamp:i):i!e

Returns which quarter of the year a moment falls in, from 1 to 4, in UTC.

time_same_day(first:i, second:i):b!e

Whether two moments fall on the same calendar date, in UTC - the same date, not within twenty-four hours of each other.

time_second(timestamp:i):i!e

Returns the second of a timestamp, from 0 to 59, in UTC.

time_sleep(seconds:f):v

Pauses the current task for the given number of seconds.

time_start_of_day(timestamp:i):i!e

Returns midnight UTC at the start of the day the timestamp falls in - the building block for everything that happened today.

time_start_of_month(timestamp:i):i!e

Returns midnight UTC on the first of the month the timestamp falls in.

time_start_of_week(timestamp:i):i!e

Returns midnight UTC on the Monday of the week the timestamp falls in.

time_start_of_year(timestamp:i):i!e

Returns midnight UTC on the first of January of the year the timestamp falls in.

time_week_of_year(timestamp:i):i!e

Returns the ISO 8601 week number, from 1 to 53. ISO weeks start on Monday and week 1 holds the year's first Thursday, so days around New Year can belong to the other year's numbering.

time_weekday(timestamp:i):s!e

Returns the day of the week written out, from Monday to Sunday.

time_workdays_between(start:i, end:i):i!e

Counts the weekday dates after the start's date, up to and including the end's date: Monday to the same week's Friday is 4, Friday to the following Monday is 1, and a same-day pair is 0 - the start's own date is never counted. An end before the start is an error.

time_year(timestamp:i):i!e

Returns the year of a timestamp, in UTC.

time_zone_offset(timestamp:i, zone:s):i!e

How far ahead of UTC a place is at a moment, in seconds. Negative is behind. The answer changes with daylight saving, which is why a moment is asked for.

time_zone_valid(zone:s):b

Whether a zone name is in the IANA database.

toml 2 functions
toml_deserialize(toml_string:s):T!e

Reads TOML into a value. The type on the left of the assignment says what to read it as, and a document that does not match names the field that did not fit.

toml_serialize(value:Any):s!e

Writes a struct, hashmap or array out as TOML - the format a person edits a configuration file in.

tui 3 functions
tui_line(text:s):TUI_Line

A plain line of the screen, in the terminal's own colour.

tui_run(initial:T):T!e

Runs a full-screen terminal program until its view reports quit, and returns the state it finished with. The program supplies two functions - view(state) and update(state, event) - and this owns raw mode, input, redrawing, resizing and putting the terminal back, including when the program panics.

tui_styled(text:s, color:TERM_Color, bold:b, selected:b):TUI_Line

A line with everything about its appearance said explicitly. A selected line is drawn with the foreground and background swapped, which is what a chosen row in a list looks like.

url 15 functions
url_build_query(params:h<s,s>):s

Builds a percent-encoded query string from a hashmap.

url_decode(text:s):s!e

Decodes a percent-encoded URL string. Errors on invalid encoding.

url_domain(url:s):s!e

The host a URL points at, with any leading www. taken off - so https://www.example.com/a?b comes back as example.com. Errors when the text is not a URL or has no host.

url_encode(text:s):s

Percent-encodes a string for safe use in a URL.

url_format(parts:URL_Parts):s

Puts a URL back together from its pieces, so a program can change one and keep the rest.

url_is_absolute(url:s):b

Returns true if the text is an absolute URL - one with a scheme and a host, so it can be fetched on its own. /about and example.com/path are not.

url_join(base:s, reference:s):s!e

Resolves a link against the page it was found on, the way a browser does: /about, ../two, ?page=2, #top and a whole URL all come out as the address to fetch. Errors if the base is not a URL.

url_origin(url:s):s!e

The origin of a URL - scheme://host, with the port when the URL named one. The piece browsers compare for CORS and cookies. Errors when the text is not a URL or has no host.

url_parse(text:s):URL_Parts!e

Takes a URL apart into scheme, user, host, port, path, query and fragment. The port is 0 when the URL did not name one. Errors when there is no scheme, since guessing turns a path into a request somewhere nobody meant.

url_parse_query(query:s):h<s,s>

Parses a query string like a=1&b=2 into a hashmap.

url_path_segments(url:s):a:s!e

The path of a URL split into its slash-separated segments, each one percent-decoded. The root path / is an empty array.

url_robots_allowed(robots_txt:s, user_agent:s, path:s):b

Whether a robots.txt file lets a user agent fetch a path - the polite scraper's question. Agent groups match case-insensitively by substring with * as the fallback, the longest matching rule between Allow and Disallow decides with Allow winning ties, * in a rule matches any run and $ anchors the end. An empty file allows everything.

url_strip_tracking(url:s):s!e

Removes the tracking parameters - utm_*, fbclid, gclid, msclkid, mc_eid - that analytics tools staple onto shared links, keeping every other query field in its original order. A URL with no query comes back unchanged.

url_to_punycode(hostname:s):s!e

A hostname written the way DNS and TLS need it, so münchen.de becomes xn--mnchen-3ya.de. This is the form a lookup or a certificate check needs. A name that is already ASCII comes back unchanged.

url_to_unicode(hostname:s):s

The readable form of a hostname stored in punycode, so xn--mnchen-3ya.de becomes münchen.de. What to show a person, having done the lookup with the other one.

validate 20 functions
validate_credit_card(text:s):b

Returns true if the digits pass the Luhn check, catching a mistyped card number before a payment is attempted.

validate_email(text:s):b

Returns true if the text is an email address that could be delivered to.

validate_hex_color(text:s):b

Returns true if the text is a hash followed by three, four, six or eight hex digits.

validate_hostname(text:s):b

Returns true if the text is a hostname: dot-separated labels of letters, digits and hyphens.

validate_iban(text:s):b

Returns true if the text is an IBAN: the right length for its country and passing the mod-97 check. Spaces are ignored. Knows the common European countries. Anywhere else is false.

validate_ipv4(text:s):b

Returns true if the text is an IPv4 address.

validate_ipv6(text:s):b

Returns true if the text is an IPv6 address.

validate_isbn(text:s):b

Returns true if the text is an ISBN-10 or ISBN-13 with a correct checksum. Hyphens and spaces are ignored.

validate_json(text:s):b

Returns true if the text is a JSON document, answered by parsing it.

validate_length_between(text:s, minimum:i, maximum:i):b

Returns true if the text has between the given numbers of characters.

validate_luhn(digits:s):b

Returns true if the digits pass the bare Luhn checksum, whatever their length - IMEIs and other identifiers as well as card numbers. Spaces and hyphens are ignored.

validate_mac_address(text:s):b

Returns true if the text is a MAC address: six pairs of hex digits separated by colons or dashes.

validate_password_strength(text:s):i

Returns how strong a password is from 0 to 4, scoring length and variety and giving nothing to the passwords everybody tries first.

validate_phone_loose(text:s):b

Returns true if the text is 7 to 15 digits once the +, spaces, dashes, parentheses and dots people format numbers with are stripped - the sanity check a signup form wants.

validate_port(number:i):b

Returns true if the number is a port a program could bind or connect to, so 1 through 65535.

validate_postal_code(text:s, country:VALIDATE_Country):b

Returns whether the text is a postal code shaped the way the given country shapes them. The country is a VALIDATE_Country variant, so there is no unknown country to be wrong about and the answer is a plain boolean.

validate_schema(json:s, schema:s):a:s!e

Checks a JSON document against a JSON Schema - types, ranges, required fields, formats. The answer is the list of problems with the path where each sits. An empty list means the document passes. The error case is a schema or document that does not even parse.

validate_slug(text:s):b

Returns true if the text is a slug: lowercase letters, digits and single hyphens, with no hyphen at either end.

validate_url(text:s):b

Returns true if the text is a URL with a scheme and a host.

validate_uuid(text:s):b

Returns true if the text is a UUID in the usual 8-4-4-4-12 spelling.

xlsx 3 functions
xlsx_read(path:s, sheet:s):a:h<s,s>!e

One sheet as rows keyed by its header row, like csv_parse. Every cell arrives as text. int_from and float_from take it from there.

xlsx_sheets(path:s):a:s!e

The sheet names in a workbook, in the order the file keeps them.

xlsx_write(path:s, sheet:s, headers:a:s, rows:a:h<s,s>):v!e

Writes one sheet from headers and rows, like csv_write. Every cell is written as text.

xml 2 functions
xml_deserialize(xml_string:s):T!e

Reads XML into a value. The type on the left of the assignment says what to read it as. Struct fields match child elements of the same name.

xml_serialize(value:Any, root_name:s):s!e

Writes a struct, hashmap or array out as XML under the given root element, for the systems that still want it that way.

yaml 2 functions
yaml_deserialize(yaml_string:s):T!e

Reads YAML into a value. The type on the left of the assignment says what to read it as, and a document that does not match names the field that did not fit.

yaml_serialize(value:Any):s!e

Writes a struct, hashmap or array out as YAML - the format CI files, manifests and compose files are written in.