Containers & Strings
Arrays ([1, 2, 3]), maps ({"key": value}) and strings are values: assigning one copies it. Six rules govern the rest:
- A method changes its receiver in place and returns nothing.
xs.sort()sortsxs. The function of the same name inarray::,map::orstring::leaves its argument alone and returns a copy:array::sort(xs). - A typed container enforces its types.
array<int>andmap<string, int>convert a compatible value and reject the rest.autoinfers the type from a uniform literal;varorarray<var>holds anything. - Reading a missing map key raises.
m[k] = vinserts;m.get(k)reads without raising. - Map keys are ordered and never coerce. Iteration and serialization walk keys in order;
1and1.0are two keys. - A string is bytes.
size(),s[i]andsubstrcount bytes ands[i]is achar. Autf8_stringcounts characters. - Every value has one display text, which
print,to_string, template strings and+onto a string all use.to_jsonwrites the textfrom_jsonreads back.
The methods
Every method name, by type: builtin methods. Every signature: the API reference.
Some names are exact synonyms, the same arguments, the same result and the same in-place behavior under either name: size = length, contains = has, remove = erase (every synonym).
Arrays
auto nums = [4, 1, 3, 2]; // an array<int>: auto infers it from the literal
nums.push(5);
print(nums.pop(), " ", nums.size(), " ", nums.front(), " ", nums.back(), " ", nums.index_of(3), " ", nums.contains(9));
nums.sort(); // in place
auto evens = array::filter(nums, x => x % 2 == 0); // a copy
print(nums, " ", evens, " ", array::slice(nums, 1, 3), " ", nums.join("-"));
nums.remove_if(x => x > 2);
print(nums, " ", nums.remove(0), " ", nums.remove(7), " ", nums);
var loose = [5, 2, 8];
try { var kept = loose.filter(x => x > 2); } catch (e) { print(e); }5 4 4 2 2 false
[1, 2, 3, 4] [2, 4] [2, 3] 1-2-3-4
[2] true false [2]
filter changes loose in place and returns nothing; array::filter(loose, f) returns a filtered copy| Method | Returns | Notes |
|---|---|---|
push(v), insert(i, v), clear() | nothing | insert takes an index 0 to size() and moves the rest up; another index raises |
pop() | the element it took | on an empty array raises Cannot pop from empty array, as front, back, first and last raise |
remove(i) | whether there was one | erases the element at index i; out of range it returns false and changes nothing |
remove_if(pred) | how many it erased | erases every element the predicate accepts |
contains(v), index_of(v) | bool; the index or -1 | compare with the language's ==: [1, 2].contains(2.0) is true, and a class's operator== is used |
sort(), sort(f) | nothing | the language's order, through a class's operator< or operator<=>; float NaNs last. f(x, y) answers whether x goes first. |
join(sep) | a string | each element's display text |
In place or a copy
| In place, returns nothing | Returns a copy |
|---|---|
xs.filter(f) | array::filter(xs, f) |
xs.sort(), xs.sort(f) | array::sort(xs), array::sort(xs, f) |
m.filter(f) | map::filter(m, f) |
s.trim() | string::trim(s) |
Every pair: in place or a copy.
- Using the result of an in-place method is an error:
auto ys = xs.filter(f);givesfilter changes xs in place and returns nothing; array::filter(xs, f) returns a filtered copy, at load when the checker knows the receiver's type, else when the call runs, before it changes anything. A chain is the same use: forxs.filter(f).map(g)writearray::map(array::filter(xs, f), g). - Dropping the result of a copying function is an error: each is
[[nodiscard]], soarray::sort(xs);as a statement fails the load ([[nodiscard]]). - Every other mutator returns nothing too (
push,clear,insert,assign,reserve, a string'strim_left,pad_center,replace_first,eraseand the rest of its in-place list, a handle'sreset), exceptpop,remove,eraseandremove_ifon containers. A class's own method of one of these names returns what it declares. slice,filterandconcatkeep the receiver's element type in both forms.xs.map(f)keeps it and converts each result as a store intoxswould;array::map(xs, f)returns anarray<T>whenfdeclares its return typeT, and anarray<var>otherwise.xs.slice(start)runs to the end, andarray::slice(xs)copies the whole array. A copy copies value elements and shares handles.
Sizing
An array sizes as a C++ std::vector does.
| Call | Effect |
|---|---|
array<T>(n) | n value-initialized elements: 0, false, an empty string or container, a null handle, or a default-constructed object |
array<T>(n, v), array<T> xs(n, v); | n copies of v |
array<int>{3} | braces list elements: [3], as std::vector<int>{3} is |
resize(n), resize(n, v) | grows with those same elements or shrinks from the end. A grow that fails (a constructor that throws) leaves the array as it was. |
assign(n, v) | makes it n elements equal to v |
fill(v) | sets every element to v; the size stays |
reserve(n) | makes room for n elements; nothing a script can see changes |
A size is a non-negative int (array size cannot be negative). An element that stays is assigned as xs[i] = v assigns, through its class's operator=; a new element is a copy.
auto grid = array<int>(4);
array<string> names(2, "?");
print(grid, " ", names, " ", array<int>{4});
grid.resize(6, 1);
print(grid);
grid.resize(2);
grid.fill(7);
print(grid, " ", array::resize(grid, 3));[0, 0, 0, 0] ["?", "?"] [4]
[0, 0, 0, 0, 1, 1]
[7, 7] [7, 7, 0]Callbacks: map, reduce, find and friends
A callback is a function, a lambda or an arrow function. It is called with the element, or with the element and its index when it declares two parameters. A predicate accepts only by answering true.
auto scores = [72, 95, 40, 88];
print(array::map(scores, x => x + 5), " ", scores.reduce((sum, x) => sum + x, 0));
print(scores.find(x => x > 90), " ", scores.find_index(x => x < 50), " ", scores.some(x => x < 50), " ", scores.every(x => x > 30));
[7, 9].for_each((x, i) => print(i, ": ", x));
array<int> hp = [10, 20];
hp.for_each([](int& x) { x -= 5; }); // a T& parameter is the element itself
print(hp);
var xs = [1, 2, 3];
int seen = 0;
for (auto x : xs) { if (xs.size() < 5) { xs.push(x + 10); } seen++; } // a range-for sees pushed elements
int called = 0;
xs.for_each((x) => { if (xs.size() < 8) { xs.push(0); } called++; }); // a helper does not
print(seen, " ", called, " ", xs.size());[77, 100, 45, 93] 295
95 2 true true
0: 7
1: 9
[5, 15]
5 5 8| Method | Returns |
|---|---|
reduce(f, init) | f(accumulator, x) folded left to right from init; reduce(f) starts from the first element and raises on an empty array |
for_each(f) | nothing |
find(f) | a copy of the first element f accepts, or null |
find_index(f) | its index, or -1 |
some(f), every(f) | whether any / every element is accepted, stopping at the first that decides. On an empty array some is false and every is true. |
- A callback parameter declared
T&names the element in the array, so a write through it changes the array, asstd::for_eachdoes in C++. A by-value parameter gets a copy. - The helpers walk the elements the array has when they start. A callback that grows the array does not see the new elements, and one that shrinks it ends the walk early. A range-
forwalks the live array: it visits elements pushed during the loop. - A
throwin a callback ends the call and propagates.
Maps
auto stock = {"sword": 1, "potion": 3}; // a map<string, int>
stock["gold"] = 250; // inserts
print(stock, " ", stock.keys(), " ", stock.values(), " ", stock.to_array());
print(stock.contains("potion"), " ", stock.get("shield"), " ", stock.get("shield", 0), " ", stock.erase("sword"), " ", stock.erase("sword"));
try { print(stock["shield"]); } catch (e) { print("caught: ", e); }
try { stock["shield"] += 1; } catch (e) { print("caught: ", e); }
for (auto name : ["orc", "elf", "orc"]) { stock[name] = stock.get(name, 0) + 1; } // counting
for (auto& [item, count] : stock) { count *= 2; } // auto& makes count the entry's value
print(map::filter(stock, (k, v) => v > 10), " ", stock.remove_if((k, v) => v < 5), " ", stock);
var ids = {1: "a"};
try { print(ids[1.0]); } catch (e) { print("caught: ", e); }{"gold": 250, "potion": 3, "sword": 1} ["gold", "potion", "sword"] [250, 3, 1] [["gold", 250], ["potion", 3], ["sword", 1]]
true null 0 true false
caught: Key "shield" not found in map
caught: Key "shield" not found in map
{"gold": 500} 2 {"gold": 500, "potion": 6}
caught: Key 1.0 not found in map| Operation | Behavior |
|---|---|
m[k] = v | inserts or overwrites |
m[k] read | raises Key "k" not found in map for a missing key and never grows the map, typed or var |
m[k] += 1, m[k]++, m[k] ??= v, var& r = m[k] | read the entry first, so they need the key to be there, where C++'s operator[] would insert one. Count with m[k] = m.get(k, 0) + 1. |
get(k), get(k, fallback) | the value, or null / the fallback |
contains(k) | whether the key is there |
erase(k) | whether the key was there |
filter(f), map::filter(m, f), remove_if(f) | f takes (key, value). filter keeps the accepted entries in place, map::filter returns them as a new map, and remove_if erases them and returns the count. |
keys(), values(), to_array() | arrays in key order; to_array gives [key, value] pairs |
- Order. Range-
for,keys(),values()and serialization walk keys in order on every platform, whatever the insertion order. Lookup is hashed. - Iterating.
for (auto [k, v] : m)binds copies of the key and the value; withauto&the value is the entry itself (a store through it converts toVor raises), and the key is still a copy.for (auto kv : m)gives a pair,kv.firstandkv.second. The loop walks key order live: a key inserted after the current one is visited, one inserted before it is not, and an erased entry is skipped. - Stored objects.
get, like an array'sfront,back,firstandlast, returns a stored object, array or map as a C++ reference return does:var x = m.get(k)is a copy, whilevar& x = m.get(k)andm.get(k).method()reach the stored value.
Key types
A map<K, V> checks its key type on every store, and keys never coerce: map<string, int> rejects m[5] (Cannot use 'int' as a key of type 'string', at load when the checker knows the key's type). A var map takes any value as a key, and still never merges kinds: {1: "a"}[1.0] raises Key 1.0 not found in map although 1 == 1.0.
| Declaration | Keys and values |
|---|---|
map<string, int> m; | an empty map of that type, ready for m[k] = v |
map<var, V> | any key |
map<auto, V>, map<K, auto>, auto m = {"a": 1} | the auto side is inferred from a uniform literal, then enforced. A mixed literal raises Map 'auto' requires homogeneous keys (or values); an empty literal leaves the side dynamic. |
Keys follow one total order, the order a map iterates and serializes in (every key kind):
| Key | Identity and order |
|---|---|
| different kinds | never one key: 1 and 1.0 are two |
| a char and its one-character string | one key, ordered as the string |
| an array, map or handle | by the identity of the object it holds, never its contents |
a class value with operator< or operator<=> | ordered by it; two keys are one when neither is less |
- Class keys iterate grouped by class name, ahead of handles.
- An operator that throws inside a lookup or insert raises from that statement and leaves the map unchanged.
- Keep a key unchanged while it is in the map. A key changed through another name leaves the order stale, as in a C++ map: erase it and insert the changed value.
- In a
parallel_forbody, a map that holds class values cannot be captured, and a map the body builds raises at its first class key whose class orders or hashes (Parallel Loops).
class Tile {
int x = 0; int y = 0;
Tile(int a, int b) { x = a; y = b; }
int operator<=>(Tile o) { return x != o.x ? x - o.x : y - o.y; }
}
var seen = {};
seen[Tile(2, 1)] = "b";
seen[Tile(1, 5)] = "a";
seen[Tile(2, 1)] = "B"; // the same key: neither is less
for (auto [t, v] : seen) { print(t.x, ",", t.y, " ", v); }
map<var, int> mixed = {};
mixed[5] = 1;
mixed["five"] = 5;
mixed['a'] = 2;
mixed["a"] = 3; // a char and its one-character string are one key
print(mixed);1,5 a
2,1 B
{5: 1, 'a': 3, "five": 5}Typed containers
array<T> and map<K, V> enforce their types on every push and store:
array<int> nums = [1, 2, 3];
nums.push(true); // converts to the element type
var bad = "nope"; // a var: the checker cannot see its type
try { nums.push(bad); } catch (e) { print("caught: ", e); }
array<var> copy = nums; // the explicit step to the dynamic form: a copy
copy.push("s");
array<float> widened = nums; // typed to typed converts element by element
auto guessed = [1, 2]; // auto infers array<int>
var loose = [1, 2]; // var: dynamic, whatever the literal held
loose.push("s");
array<array<int>> grid = [[1, 2], [3, 4]];
print(nums, " ", copy, " ", widened, " ", loose, " ", grid[1][0]);
array<uint8> pixels = [0, 128, 255];
pixels[1] += 100;
try { pixels[2] += 1; } catch (e) { print("caught: ", e); }caught: Cannot push 'string' to array<int>
[1, 2, 3, 1] [1, 2, 3, 1, "s"] [1.0, 2.0, 3.0, 1.0] [1, 2, "s"] 3
caught: Cannot store 256 in array<uint8> (0..255)| Rule | Detail |
|---|---|
| conversions into an element or a map value | int into float (a float into an int element needs cast<int>(x)), and bool ↔ int (true is 1; an int is false only when zero). float ↔ bool is rejected, and a number into a string needs to_string(v) or cast<string>(v). |
| rejection | an error naming the container (Cannot push 'string' to array<int>), at load for a literal, a typed push argument or a typed subscript store, and at run time (catchable) for a var. A rejected push leaves the array as it was. [[warn_mode]] downgrades the load error. |
| null | only shared_ptr<T> and weak_ptr<T> elements admit null. A shared-handle element also takes a live instance of T or a subclass. |
| inference | auto infers array<T> or map<K, V> from a uniform literal and enforces it; a mixed literal raises Array 'auto' requires homogeneous elements - type mismatch at index 1. A literal with no declared type under var is dynamic. |
a typed array in a var | stays typed: after var v = ints;, v.push("s") raises, while v = "hello" is fine |
| typed to typed | array<float> f = ints; converts element by element, by value; array<var> v = ints; is the dynamic copy |
| class elements | array<Base> holds a Base or any subclass, and each element keeps its own class and fields (cast<Archer&>(squad[1]).range). array<Derived> converts to array<Base> by value. An unrelated class raises Cannot push 'Rock' to array<Unit>. |
| declared without an initializer | empty, as every typed declaration takes its type's default (Type Ladder) |
The declared element type is the storage. An array<int> or array<float> is a raw buffer of int64s or doubles, and every other element type but var (array<math::vec3> included) is packed the same way, so a typed loop and a C++ binding read it without boxing (performance notes).
Byte arrays. array<uint8> (0 to 255) and array<int8> (-128 to 127) hold one byte per element, and an element reads as an int.
- Every store checks the range:
Cannot store 256 in array<uint8> (0..255), from=, a compound assignment,++,push, a reference or a conversion. - A
boolstores 0 or 1. A float and acharneedcast<int>. array<int> x = bytes;andarray<uint8> b = ints;convert by value; the second checks every element.uint8andint8name array elements only:uint8 x = 5;is a parse error.
Printing values
print, to_string, a template splice, format's {}, join, + onto a string, a string parameter or variable, and a throw's message all use a value's display text:
| Value | Display text |
|---|---|
| a string | its bytes. Inside a container a string or utf8_string is quoted with " and a char with ', with \n, \r, \t, \\ and the quote escaped and other control bytes as \xNN. |
a number, bool, null, a vector | 2.0, true, null, (1.0, 2.0) |
| an array, a map | [1, 2, 3]; {"x": 1, "y": [2]} in key order |
| an instance | its string to_string() (its own or a base's); otherwise its class name and fields in declaration order, a base's first: P{x: 1, name: "hi"} |
a shared_ptr or weak_ptr | the object it points to; an expired weak_ptr is null. An object met again while it is being shown prints <cycle P>; one held twice side by side prints twice. |
| a function, a coroutine handle | <function>, <coroutine> |
a host object with no bound to_string | its type name: <Sprite> |
A value prints in full however large or deep it is. A to_string() that throws is the error of the call that asked for the text.
class P { int x = 1; string name = "hi"; }
class Q : P { array<int> hits = [2, 3]; }
class Tag { string to_string() { return "<tag>"; } }
class Link { int id = 0; shared_ptr<Link> next; }
print([1, 2.5, "two", 'c', true, null], " ", {"name": "orc\n"});
print(Q(), " ", [Tag(), Tag()]);
shared_ptr<Link> a = new Link();
a.next = a;
print(a, " ", "tags: " + [Tag()]);[1, 2.5, "two", 'c', true, null] {"name": "orc\n"}
Q{x: 1, name: "hi", hits: [2, 3]} [<tag>, <tag>]
Link{id: 0, next: <cycle Link>} tags: [<tag>]JSON
to_json(x) writes any value as compact JSON, and to_json(x, n) indents it n spaces per level. from_json(text) reads it back; a malformed text raises a catchable error. The same data always writes the same text, since map keys come out in order.
var save = {"name": "orc", "hp": [10, 20], "pos": {"x": 1.5}};
print(to_json(save));
print(to_json({"hp": [10]}, 2));
map<int, string> slots = {1: "sword"};
var back = from_json(to_json(slots)); // int keys survive the round trip
print(to_json(slots), " ", back[1], " ", to_json('A'), " ", to_json(1.0 / 0.0));
class Hero { string name = "Vex"; int hp = 30; }
Hero h;
h.hp = 12;
Hero copy = from_json(to_json(h));
print(to_json(h), " -> ", copy.name, " ", copy.hp);
try { from_json("{bad"); } catch (e) { print("caught: ", e); }{"hp":[10,20],"name":"orc","pos":{"x":1.5}}
{
"hp": [
10
]
}
{"$map":[[1,"sword"]]} sword {"$char":65} {"$float":"inf"}
{"$type":"Hero","$val":{"name":"Vex","hp":12}} -> Vex 12
caught: Expected '"' in JSONA value JSON cannot spell is written as a tagged object ($map, $char, $float, $type, $id, $ref): JSON encoding.
from_jsonrebuilds an instance when its class exists, and a typed declaration takes it directly (Hero h = from_json(text);). It runs the class's zero-argument constructor whatever its access, restores every saved field in declaration order without notifying, then runspost_loadonce the whole load is done (Loading saved objects).- Handles that shared one object share it again after loading, through cycles too. An id the text never defined raises
Invalid shared_ptr ID: 4. to_binaryandfrom_binaryround-trip the same values and identities in a binary form.- No depth limit applies: a value nests as deep as memory allows, in JSON, binary and a copy alike.
- JSON and binary archives for C++ objects are in Serialization.
Strings
The reading methods return information, the in-place methods change the string and return nothing, and the string:: functions return a changed copy. auto upper = name.to_upper(); is an error (to_upper changes name in place and returns nothing; string::to_upper(name) returns an uppercased copy), as is the chain t.trim().to_upper().
auto s = "The Quick Fox";
print(s.length(), " ", s.find("Quick"), " ", s.find("Slow"), " ", s.count("o"), " ", s.substr(4, 5), " ", s.at(-1));
print(s.contains("Fox"), " ", s.starts_with("The"), " ", s.ends_with("Fox"), " ", s.partition(" "), " ", "b".compare("a"));
print("42".to_int() + 1, " ", "2.5".to_float(), " ", "abc".to_int(), " ", "abc".to_int() ?? -1);
print(" stats 35\t25 ".split(), " ", "a,,b".split(","), " ", "a,b,c".split(",", 1), " ", "ab".split(""), " ", "a\r\n\nb\n".lines());
var t = " hello world ";
t.trim();
t.to_upper();
t.replace("WORLD", "JAI");
var id = "7";
id.pad_left(3, '0');
print(t, " ", string::to_lower(t), " ", id, " ", string::repeat("-", 5), " ", string::join(["a", "b"], "+"));
var built = "a";
built += 'b'; // a char appends as its character
built = built + "c" + [1]; // an object or container joins its display text
try { built += 5; } catch (e) { print("caught: ", e); }
print(built);13 4 -1 1 Quick x
true true true ["The", " ", "Quick Fox"] 1
43 2.5 null -1
["stats", "35", "25"] ["a", "", "b"] ["a", "b,c"] ["a", "b"] ["a", "", "b"]
HELLO JAI hello jai 007 ----- a+b
caught: Cannot apply '+=' to string and int
abc[1]| Method | Behavior |
|---|---|
find(text) | the byte position, or -1 |
substr(start, len) | len bytes from start |
at(i) | the char s[i] reads; a negative i counts from the end |
to_int(), to_float() | the number, or null when the text is not one. A typed slot refuses the null (int n = "abc".to_int(); raises Cannot initialize 'n' (declared int) with null), so supply a default: s.to_int() ?? 0. |
split() | splits on whitespace (space, tab, newline, carriage return, vertical tab, form feed) and drops empty pieces; split(string::whitespace) spells it out |
split(sep), split(sep, max) | cuts at each exact separator and keeps empty pieces; max caps the number of splits and the rest stays in the last piece |
split("") | characters: bytes for a string, characters for a utf8_string |
lines() | \n and \r\n both end a line, blank lines stay, and a trailing newline adds no empty last element |
partition(sep) | [head, sep, tail]; [s, "", ""] when sep is absent |
a.compare(b) | -1, 0 or 1 |
is_digit, is_alpha, is_alnum, is_space, is_upper, is_lower | true when the string is non-empty and every byte is in the ASCII class. Case mapping is ASCII too. |
replace(from, to) | every occurrence, left to right without overlap ("aaaa" with "aa" → "b" is "bb"); replace_first and replace_last replace one |
pad_left(n, fill), pad_right, pad_center | pad to n with spaces or a fill; a fill of several characters tiles from its start ("ab".pad_left(7, "xyz") is "xyzxyab"), and an empty one raises pad_left() padding character cannot be empty |
repeat(n) | a negative count raises string::repeat count cannot be negative |
- The copying functions are listed by name (the pads take an optional fill:
string::pad_left(s, n, [fill]));string::join(parts, [sep])joins an array of strings. - The namespace is open:
namespace string { function shout(string s) -> string { return string::to_upper(s) + "!"; } }addsstring::shout. +joins text only: strings, chars, and the display text of an object or container. A number, bool ornullbeside a string is an error (Operators).+=is the string builder.s += x,s = s + xands = s + a + bappend to the string's own buffer, on a local, a field, an element, a map value or through a reference, so a loop that builds a page costs the bytes it appends (the cost model). A copy taken along the way keeps the text it had.
Bytes and chars: s[i], \xNN, cast<char>
s[i] reads a char. In arithmetic, bitwise operators and comparisons with numbers a char is an integer 0–255, always unsigned ("\xC3"[0] + 0 is 195), the unary -c, +c and ~c included: -'a' is -97. Beside a string it is text (Operators). The byte-work idioms:
var s = "Jai\x21"; // \xNN: exactly two hex digits
print(s, " ", s[0], " ", type_of(s[0]), " ", s[0] + 0, " ", "7"[0] - '0', " ", s[3] == 0x21);
print((s[0] + 0) | ((s[1] + 0) << 8)); // a little-endian u16 from two bytes
var out = "";
out += to_char(0x4A); // int -> char, range-checked 0..255
for (auto c : "ab") { out += c; } // range-for binds each char by value
print(out);
string name = "jai";
name[0] = 'J'; // a char replaces byte 0
name[2] = "i!!"; // a string replaces byte 2 with its whole text
name[1] += "h"; // name[1] = name[1] + "h"
print(name);
var five = 5;
try { name[0] = five; } catch (e) { print("caught: ", e); }Jai! J char 74 7 true
24906
Jab
Jahi!!
caught: Cannot assign int to a string character (it takes a char or a string)- A subscript is a store target.
s[i] = creplaces bytei, ands[i] = textreplaces it with the whole text, so the string grows or shrinks. A compound store reads the char, applies the operator and writes the result back. Anything but a char or text raisesCannot assign int to a string character (it takes a char or a string), a load error when the checker sees the types. A copy of the string taken before the write keeps its bytes. - An index past the end raises
String index 9 out of bounds for string of size 5, read or write. for (auto c : s)binds each char by value;auto&raises, since a char read from a string is not a variable.- Chars and numbers convert only through
cast:cast<int>(c)andcast<float>(c)give the code,cast<string>(c)the one-character string, andcast<char>(n)orcast<char>("a")a char (Cannot cast 'string' to 'char'for a longer string).to_char(n)is the function form.
Binary data
A string holds any byte 0–255, NULs included, and read_file / write_file move bytes unchanged, so a file's bytes are a string.
| Call | Result |
|---|---|
s.to_bytes(), s.to_bytes(offset), s.to_bytes(offset, count) | an array<uint8> (on a utf8_string, its UTF-8 bytes). The range is checked: to_bytes(1, 3) out of bounds for string of size 3. |
string::from_bytes(bytes) | a string from an array<uint8> or array<char>, or from an array<int> or array<var> whose every value is 0–255 (else string::from_bytes expects bytes (0 to 255): element 3 is 256) |
read_file(path) | the file's bytes; an empty file is "". A missing or unreadable file raises Cannot read file 'saves/world.dat': not found (or not a file, permission denied). Ask first with file_exists(path) (Where files are). |
write_file(path, text) | writes the bytes exactly: embedded NULs, \r\n untouched |
var ENTRY = binary::layout("pos", binary::int32, "size", binary::int32, "name", binary::text(8));
binary::writer w; // a tiny WAD: header, one lump, a directory
w.bytes("PWAD").int32(1).int32(0); // id, lump count, directory offset (patched below)
int lump = w.pos();
w.bytes("hello");
int dir = w.pos();
w.pack(ENTRY, {"pos": lump, "size": 5, "name": "GREET"});
w.seek(8).int32(dir); // back to the header to patch the offset
string wad = w.to_string();
var r = wad.reader();
print(r.text(4), ", lump count ", r.int32());
r.seek(r.int32()); // the directory
var e = r.unpack(ENTRY); // map<string, var>: name -> value
print(e["name"], " at ", e["pos"], ": ", wad.substr(e["pos"], e["size"]));
class Entry { int pos; int size; string name; }
Entry first = wad.reader().seek(dir).read(Entry, ENTRY);
print(first.name, " ", first.pos, " ", first.size, " ", wad.to_bytes(0, 4), " ", string::from_bytes([80, 87, 65, 68]));
try { r.int32(); } catch (err) { print("caught: ", err); }
try { w.uint8(256); } catch (err) { print("caught: ", err); }PWAD, lump count 1
GREET at 12: hello
GREET 12 5 [80, 87, 65, 68] PWAD
caught: Cannot read int32 at position 33: 4 bytes wanted, 0 remain
caught: Cannot write 256 as uint8 (0 to 255)binary::reader. s.reader() or binary::reader(s) reads the string from a cursor at 0, little-endian. Every read advances the cursor. Every call: binary::reader.
- Reading past the end raises
Cannot read int32 at position 10: 4 bytes wanted, 2 remainand leaves the cursor where it was. text()never scans past the end of the data or past the host'sengine::binary_scan_limit(1 MiB unless the host sets it; a script cannot change it). With no NUL in reach it raises and leaves the cursor:Cannot read null-terminated text at position 5: no NUL terminator in the remaining 3 bytes, or... no NUL terminator within 1048576 bytes (the engine's binary scan limit; raise it from C++ with engine->binary_scan_limit).- A reader is a value:
auto r2 = r;copies the cursor, and abinary::reader¶meter shares it. It reads the string as it was when the reader was made.
binary::writer. binary::writer w; mirrors the reader, and each call returns the writer. Every call: binary::writer.
Layouts. binary::layout(name, width, ...) builds a reusable layout, validated once (binary::layout fields total more than 9223372036854775807 bytes). The widths are binary::int8, int16, int32, int64, uint8, uint16, uint32, float32, float64, binary::text(n) and binary::bytes(n): values that describe an encoding, not types. binary::bytes(n) unpacks to an array<uint8> and packs exactly n bytes. Field order comes from the arguments, never from a map. The checker reports at load a wrong signature, an odd name/width pairing, a non-width where a width belongs, and a read(Class, layout) whose class and layout do not match. As a table: layouts and widths.
UTF-8 text
A string counts bytes. Its UTF-8 methods read characters without changing that, and a utf8_string holds the same bytes with every position a character:
string s = "héllo 🙂";
print(s.size(), " bytes, ", s.utf8_length(), " characters, valid: ", s.is_valid_utf8());
print(s.utf8_chars(), " ", "é".code(), " ", string::from_code(233));
utf8_string name = "Zoë 🙂";
print(name.size(), " ", name[2], " ", name.byte_size(), " ", name.substr(0, 3), " ", type_of(name[2]));
for (auto c : name) { print(c, skip_newline); }
string raw = name; // the same bytes, counted as bytes
print(" ", raw.size(), " ", name == raw);
utf8_string u = "abc";
u[1] = "日";
print(u, " ", u.size(), " ", u.byte_size());11 bytes, 7 characters, valid: true
["h", "é", "l", "l", "o", " ", "🙂"] 233 é
5 ë 9 Zoë utf8_string
Zoë 🙂 9 true
a日c 3 5On a string | Result |
|---|---|
utf8_length() | the number of characters |
utf8_chars() | each character as a string |
is_valid_utf8(), string::is_valid_utf8(s) | whether every byte belongs to a valid sequence |
code() | the code point of a string of exactly one character |
string::from_code(n) | the character; raises for a surrogate or a value past U+10FFFF |
to_utf8(), utf8_string(x), a utf8_string declaration | a utf8_string |
utf8_string: text by character
- On a
utf8_string,size(),length(),u[i],at,substr,find,insert,erase, the pads,reverse,split("")and range-forcount and return characters. A character is a one-characterutf8_string;byte_size()counts bytes. - A byte that starts no valid sequence is one character of its own, everywhere, and keeps its byte:
"a\xFFb"is three characters. No text is ever lost. - A
stringvariable or a C++std::stringparameter takes autf8_stringas its bytes, and the two compare and order by bytes:u == sholds for the same text. +with autf8_stringon either side makes one, and a method orstring::function that returns text returns the receiver's kind.u[i] = textreplaces characteriwhatever its byte width, andu[i] += textappends to it.- Positions are cheap: ASCII text indexes like bytes, a loop over
u[i]steps a character at a time, and random reads index a long text once.
Digests, HMACs and password hashes are crypto:: functions (Random Numbers & Crypto).
Template strings & format specs
A backtick string splices any expression with ${...}, as its display text. It takes the escapes of a double-quoted string plus \` and \$, and a line ending inside the backticks is one \n. A splice, and a {} placeholder of print or format, can end in a format spec, ${expr:spec}:
[[fill]align][+| ][0][width][,|_][.precision][d|x|X|b|f|e|g]
var name = "Vex";
var hp = 73.5;
var gold = 1234;
print(`${name:<8}|${gold:8}|${gold:*>8}|${gold:^7}|`);
print(`${hp:.1f} ${hp:>8.2f} ${gold:05} ${-42:05} ${gold:+} ${gold:x} ${gold:X} ${5:b}`);
print(`${gold * 1000:,} ${hp * 1000:,.2f} ${gold:_b} ${1234:010,}`);
var crit = true;
print(`${crit ? "CRIT" : "hit"} for ${(crit ? 150 : 100):>5}|`);
try { print(`${name:.2f}`); } catch (e) { print("caught: ", e); }
print(format("{:.2f} vs {0:>8}", 3.14159));Vex | 1234|****1234| 1234 |
73.5 73.50 01234 -0042 +1234 4d2 4D2 101
1,234,000 73,500.00 100_1101_0010 00,001,234
CRIT for 150|
caught: format spec ':.2f' does not apply to string value
3.14 vs 3.14159Each part: format specs.
- A spec that does not fit the value raises when it runs:
format spec ':.2f' does not apply to string value. - A malformed spec (
${x:q},${x:#x},${n:,x}) in a template string is a load error,Unsupported format spec ':q' in template string; in aformatorprintplaceholder it raisesUnsupported format spec ':q' in format stringwhen the call runs. - The spec starts at a top-level single
:that closes no ternary, so${a ? b : c}stays an expression and${m["k"]:6}works. Parenthesize a ternary to give it a spec. ::is scope resolution, never a spec:${::hp}reads the globalhp, and${::hp:5}pads it.format_value(value, "spec")is the builtin behind a splice, on every engine.
Regular expressions
regex(pattern) and regex(pattern, flags) make a regex value. Write the pattern as a raw string, R"(\d+)", so backslashes stay backslashes. Matching is linear in the text: no pattern backtracks, so a hostile pattern or text cannot stall a search.
regex date = regex(R"((?<year>\d{4})-(?<month>\d\d)-(?<day>\d\d))");
var m = date.search("due 2026-10-21, paid 2026-11-02");
print(m.text(), " at ", m.start(), "..", m.end(), ": ", m[1], " / ", m["month"], " (", m.size(), " slots)");
for (var each : date.find_all("2026-10-21 2026-11-02")) { print(each["day"], " at ", each.start()); }
print(date.replace("on 2026-10-21", "${day}.${month}.${year}"), " ", date.search("no dates") == null);
print(regex(R"(\d+)").replace("a1b22c333", (x) => "<" + to_string(x.text().size()) + ">"));
print(regex(R"(\s*,\s*)").split("red , green,blue"), " ", regex("(,)").split("a,b"), " ", regex(",").split("a,b,c", 1));
print(regex(R"(^[a-z_]\w*$)", "i").matches("Player_2"), " ", regex::escape("1+1=2?"));
print(regex("x*").replace("abc", "-"), " ", regex(R"((\w+) (\w+))").replace("ada lovelace", "$2, $1 ($$5)"));
string backref = R"((\w+)\1)";
try { regex(backref); } catch (e) { print("caught: ", e); }2026-10-21 at 4..14: 2026 / 10 (4 slots)
21 at 0
02 at 11
on 21.10.2026 true
a<1>b<2>c<3>
["red", "green", "blue"] ["a", ",", "b"] ["a", "b,c"]
true 1\+1=2\?
-a-b-c- lovelace, ada ($5)
caught: regex: the backreference '\1' at position 5 is not supported: matching runs in linear time, without backtracking| Call | Answers |
|---|---|
r.search(text) | the leftmost match, a regex::match, or null |
r.matches(text) | whether the whole text matches |
r.find_all(text) | every match, left to right, as an array of regex::match |
r.replace(text, with) | the text with every match replaced by a replacement text, or by what a function returns for the match |
r.split(text) | the pieces between matches |
Every call, and what a regex::match answers: regex calls.
A match is a value: it holds its own texts. to_json writes a regex as its pattern and flags, and from_json and from_binary compile it again; a regex::match is not saved state.
The syntax
Every construct: regex syntax. The flags i, m, s and x: regex flags.
Another flag letter, or one twice, raises. Among the matches that start leftmost, the one an alternation's order and each repetition's greed reach first wins, as in ECMAScript, Perl and PCRE. A loop pass that matches empty ends the loop.
Constructs that need backtracking are errors that name the construct (regex: the backreference '\1' at position 5 is not supported: matching runs in linear time, without backtracking): backreferences (\1, \k<name>), lookahead and lookbehind ((?=, (?!, (?<=, (?<!), atomic groups ((?>), possessive quantifiers (*+) and recursion ((?R)). A pattern whose compiled form grows past the engine's limit ((a{1000}){1000}) raises too.
Replacements, empty matches and text kinds
A replacement text inserts a group as $1, ${name} or $0 (the whole match) and a dollar sign as $$: replacement text.
- A group that took no part inserts nothing. A group the pattern lacks, or a
$before anything else, raises:regex: the replacement names group 2 at position 0, but the pattern has 1 group. - A function replacement is called once per match, in order.
- Empty matches follow ECMAScript: after one, the next search starts one character on. So
regex("x*").replace("abc", "-")is-a-b-c-, andsplitsplits nothing at a piece's start or at the end of the text.
A string matches by byte, a utf8_string by character.
- On a
string,.and a class match one byte, positions count bytes, and a non-ASCII character in the pattern matches its UTF-8 bytes. A class that holds a multi-byte character raises on a string (match autf8_string, or write the bytes as\xNN). - On a
utf8_string,.and classes take whole characters, positions count characters, an empty match moves on by a whole character, and the texts come back asutf8_string. A byte that starts no valid sequence is matched by nothing and counts as one character in a position.
A constant pattern compiles once, at load, when the pattern and flags are string literals (a raw string, a + of literals, or a const string the file declares from literals); an invalid one is a load error with its position (regex: missing ')' to close the group at position 0). A pattern built at run time compiles when the call runs and raises the same texts, catchably; the engine keeps recent patterns compiled. Regex calls run inside a parallel_for body, except a function replacement (Parallel Loops).
Parallel loops
parallel_for, parallel_transform and parallel_reduce run a loop over an array on every core, with the serial loop's results: Parallel Loops.