Chapter 9

Strings Are Bytes

Goose has no string type. A string is an array of u8, and which member of the array family you choose says what you want to do with it. This turns out to be a good fit for the memory model: building text, storing text inside records and cutting text into pieces each get the representation that suits them, and none of them needs a heap.

#Four kinds of string9.1

Type Use it for
u8[>..] building text: appending is a pointer bump
u8[] finished text that owns its bytes, such as a name in a record
u8[..k] short text inside a struct, with room for k bytes
u8[:] a view of bytes that live somewhere else
u8[>..] 4 h o n k a builder: grows at the top of its own stack u8[] 4 h o n k finished: length and bytes, frozen u8[..8] 4 h o n k room for 8, inside its owner u8[:] ptr 4 a view: somebody else's bytes, 16 bytes however long the text is a string literal is a const u8[:] into static data
The four everyday kinds of string. Three of them hold their bytes; the slice only views bytes held elsewhere.

A string literal is a view: a const u8[:] of bytes in the program's read-only data. It is read-only, which is why some slots have to be declared const before they can hold a literal, as you will see below.

No encoding is enforced. Strings are bytes, and the standard library treats them as UTF-8 by convention where it matters.

#Building text9.2

A u8[>..] is a string builder. append adds the bytes of another string, and push adds a single byte. The built-in format appends the text form of any values, exactly as print would write them:

fn main() {
    var s: u8[>..] = [];
    s.append("honk");
    s.push('!');
    format(s, " x", 3, ", weight ", 4.5);
    print(s, " [", s.len, " bytes]");
}
honk! x3, weight 4.5 [20 bytes]

str(…) takes the same arguments as print, and returns the text as a new builder. Like every value in Goose it is built directly in its destination, so names.push(str("goose #", i)) writes the text straight into the new element:

fn main() {
    let msg = str("eggs: ", 12, ", geese: ", 3);
    print(msg, " (", msg.len, ")");
}
eggs: 12, geese: 3 (18)

Remember that a character literal is an integer. str('g') is the text "103", not "g"; to add one byte, use push.

#Literals and escapes9.2.1

String literals use the escapes you know: \n, \t, \r, \0, \\, \", \', and \xNN for any byte. A raw string is written between """ delimiters and has no escapes at all. A multi-line raw string has its closing delimiter's indentation removed from every line, so it can be indented with the code around it:

fn main() {
    print("tab\there, quote \" and \x41");
    print("""C:\geese\list.txt""");
    print("""
        Usage: honk [-n count]
          -n   how many times
        """);
}
tab here, quote " and A
C:\geese\list.txt
Usage: honk [-n count]
  -n   how many times

#Storing text9.3

Text that is finished and needs a home goes into a u8[] or a u8[..k]. Initializing one from any string, including a literal or a slice, copies the bytes into it, so the new value owns its text:

fn main() {
    var draft: u8[>..] = [];
    draft.append("Ada of the pond");
    let name: u8[] = draft[..3];     // three bytes of its own
    draft.clear();                   // fine: name does not point into draft
    print(name);
}
Ada

A u8[..k] is the natural choice inside a struct, because it keeps the struct fixed-size, which in turn keeps an array of such structs indexable and sortable. A limited array is also a builder up to its capacity, so you can format into one:

struct Goose { name: u8[..12], eggs: i32 }

fn main() {
    var g = Goose { name: "Ada", eggs: 0 };
    format(g.name, " Jr.");
    print(g.name, " ", g.name.len, "/", g.name.cap);
}
Ada Jr. 7/12

When a struct holds a view rather than its own bytes, its field type says whether the view may be read-only. A field declared u8[:] holds a writable view, and a string literal cannot go there. Declare it const u8[:] and it can:

struct Label { text: const u8[:] }

fn main() {
    let l = Label { text: "fragile: contains eggs" };
    print(l.text);
}
fragile: contains eggs

#Printing values inside text9.4

Every value has a text form. Strings print as their bytes, but a string inside another value, such as an array or a struct, is quoted and escaped, so you can tell where it begins and ends:

struct Goose { name: u8[..12], eggs: i32 }

fn main() {
    let name: u8[] = "Ada";
    print(name, " ", [name], " ", Goose { name: "Bea", eggs: 2 });
}
Ada ["Ada"] Goose { "Bea", 2 }

To give your own type a text form of its own, write an overload of format that takes the type. print, str and format all use it, anywhere the type appears, including inside arrays:

struct Goose { name: u8[..12], weight: f32 }

fn format(out: u8[>..]&, g: Goose) {
    format(out, g.name, " (", g.weight, " kg)");
}

fn main() {
    let flock = [Goose { "Ada", 4.5 }, Goose { "Bea", 3.75 }];
    print(flock);
    print("leader: ", flock[0]);
}
[Ada (4.5 kg), Bea (3.75 kg)]
leader: Ada (4.5 kg)

#Cutting text up9.5

The text functions are in the std module, so they need import std;. Almost all of them take a u8[:] and return slices of it. Cutting text up copies nothing: the pieces are views into the original bytes.

import std;

fn main() {
    let line = "  name = Ada Lovelace  ";
    let body = trim(line);
    let eq = find(body, "=");
    print("[", trim(body[..eq]), "] [", trim(body[eq + 1..]), "]");
}
[name] [Ada Lovelace]

find returns the position of the first occurrence, or -1. trim, trim_start and trim_end remove ASCII whitespace by returning a smaller slice.

split cuts a string at a separator and returns a growable array of slices; words splits at runs of whitespace and drops the empty pieces:

import std;

fn main() {
    let parts = split("Ada,4.2,7", ',');
    print(parts.len, " parts: ", parts);
    print(words("  waddle   to  the pond "));
}
3 parts: ["Ada", "4.2", "7"]
["waddle", "to", "the", "pond"]

Each part takes sixteen bytes, a pointer and a length. None of the text was copied. If you do not need the array at all, each_split and each_word call a block for each piece instead. The { … } after the call is a trailing block, a small function passed to each_split, with it as its argument. Chapter 16 covers them properly.

import std;

fn main() {
    each_split("north-east-south", '-') { print("go ", it); };
}
go north
go east
go south

#Converting to and from numbers9.6

parse_int and parse_flt read a number from the whole of a string, and return it together with a bool that says whether the text was valid:

import std;

fn main() {
    let n, ok = parse_int("7");
    let w, wok = parse_flt("4.2");
    let bad, bok = parse_int("seven");
    print(n * 2, " ", ok, " ", w, " ", wok, " ", bad, " ", bok);
}
14 true 4.2 true 0 false

Going the other way, format writes numbers in their natural form. When you want control over it, format_int takes a base, a minimum width and a padding byte, and format_flt a number of decimals:

import std;

fn main() {
    var out: u8[>..] = [];
    format_int(out, 255, 16, 6, '0');
    out.append("  ");
    format_flt(out, 3.14159, 2);
    print(out);
}
0000ff  3.14

#Comparing and changing text9.7

Strings compare with == like any arrays, and this works across kinds: a builder against a literal, a u8[..12] field against a slice. For ordering, compare(a, b) returns -1, 0 or 1. to_lower and to_upper change ASCII letters in place, so they need a writable string. join writes several strings into a builder with a separator between them:

import std;

fn main() {
    var call: u8[>..] = [];
    call.append("Honk");
    to_upper(call);
    var out: u8[>..] = [];
    join(out, ["ada", "bea", "cleo"], ", ");
    print(call, " ", call == "HONK", " ", compare("ada", "bea"), " ", out);
}
HONK true -1 ada, bea, cleo

#Bytes and characters9.8

Indexing a string gives a byte, a u8, which prints as a number. A string literal's .len is its length in bytes, which is not the number of characters once UTF-8 is involved. each_utf8 decodes the code points for you, and push_utf8 appends one:

import std;

fn main() {
    let word = "gänse";
    var points = 0;
    each_utf8(word) { points += 1; };
    print(word[0], " ", points, " code points in ", word.len, " bytes");
}
103 5 code points in 6 bytes

#Exercises9.9

  1. Write a function initials(full_name: u8[:]) -> u8[>..] that returns the first letter of every word, so that initials("ada byron lovelace") is "abl".

  2. Parse the line "Ada,4200;Bea,3900;Cleo,4600" into the total weight of all three geese. Use split twice, or each_split twice.

  3. Write a format overload for struct Egg { day: i32, grams: f32 } that prints an egg as day 3: 61.5 g, and print an array of two eggs.