Most languages have several string stories that grew together: C’s bytes with a terminator, Java’s immutable objects plus a builder bolted on later, Python’s str-versus-bytes schism. Mica has one story with three deliberate shapes, each earning its place: string the immutable value, stringbuffer the builder, stringpart the borrowed window. This tutorial walks all three plus the strings unit’s verbs, in one compiled example.

It assumes values — a string is a value under the same rule as everything else — and pairs with UtfSources, which owns the encoding story.

The value

A string is a two-field value: a pointer to frozen data, and a length counted in runes, not bytes. Nothing in the language ever mutates a string’s content — every operation answers a new string — so a string you hold can never change behind your back, and passing one costs two machine words, not a copy of the text.

line := "disk" + " " + "full";
WriteLn("  built: %ls", line);
WriteLn("  runes: %lld", Length(line));
if line = "disk full" then
    WriteLn("  comparison reads content, not identity");
The value
  built: disk full
  runes: 9
  comparison reads content, not identity

Immutability is compiler-enforced, and the refusal names the escape hatch:

analyzer error 5225: a string element cannot be assigned: a string value is
immutable, so 'line[...]' is read-only — build changing text in a
stringbuffer and materialize it with ToString

The walk: runes, not bytes

for ch in s decodes one code point per step; the control variable is a unicode, printed with %lc. Indexing s[i] answers the same type. The umlaut counts once, the emoji counts once:

The walk: runes, not bytes
  'größe 😀' walks 7 runes, Length says 7
  first g, last 😀

Whether those runes are stored four bytes apiece or in varying widths is the build’s business, not the program’s — UtfSources makes that choice visible.

The unit: verbs over values

The everyday verbs are not ambient; they live in the strings unit and arrive by import like everything in Mica:

imp
    Trim, UpperCase, StartsWith, Pos, Copy, Split, Join, Parts : strings;
trimmed := Trim(line);
upper := UpperCase(Copy(trimmed, 0, 5));
fields := Split("a,b,,c", ",");
joined := Join(fields, " | ");
The unit: verbs over values
  trimmed: [error: disk full]
  shouted head: ERROR
  starts with 'error' at position 0
  split and rejoined: a | b |  | c

Every verb answers a new string; the pipeline refines copies and the originals stand. Split keeps empty pieces (the b | | c above), and Join is its exact inverse over the same separator. The full surface — Contains, EndsWith, Replace, Str/Val and their float twins, case mapping — follows the same shape: value in, value out.

The builder

+ inside a loop copies everything built so far on every round — quadratic, however fast each copy is. A stringbuffer appends in amortized constant time; Reserve pre-sizes it when the total is known; ToString materializes an exact-sized immutable string; Clear empties for reuse without releasing the backing:

Reserve(address buf, 32);
for n := 1 to 5 do
    Append(address buf, "ab");
line := ToString(buf);
Append(address buf, "!!!");
The builder
  materialized: ababababab (still 10 runes after the builder grew)
  after Clear: fresh

Two details carry the value rule through. The builder is passed by address — it is mutable state, so every touch is consented to at the call site, as always. And ToString is a real copy: the !!! appended afterwards never reaches the materialized string.

The window: tokenizing without allocating

Split allocates its pieces. A parser’s inner loop must not — so the same walk exists as a cursor of three integers and a stringpart: a borrowed window into the subject, no copy, no allocation, one pass over the subject’s bytes for the whole walk:

cursor := SplitStart();
while Splitting(address cursor) do
begin
    piece := NextPart(line, ",", address cursor);
    n := n + 1;
    WriteLn("  piece %lld: [%ls] (%lld runes)", n, piece, Length(piece));
end;
The window: tokenizing without allocating
  piece 1: [eins] (4 runes)
  piece 2: [zwei] (4 runes)
  piece 3: [] (0 runes)
  piece 4: [vier] (4 runes)

A window prints, measures and compares like a string — but it borrows from its subject, and the borrow rules bound each window’s life to the subject’s, the same discipline alias taught on the heap. A piece that must outlive the walk is materialized with ToString — the visible exception, exactly where the allocation belongs.

What the compiler refused

The program tried toThe compiler said
assign into a string element5225: … a string value is immutable, so 'line[...]' is read-only — build changing text in a stringbuffer and materialize it with ToString
print a string with %d5184: format specifier '%d' at position 1 incompatible with type: string

What this does not do

  • No in-place mutation, anywhere. There is no “small write” exception; changing text is what the builder is for, and 5225 will keep saying so.
  • Indexing is by rune position. Under the utf-8 build a positional s[i] in a hand-written loop walks encoding boundaries — the taught idiom for traversal is for ch in s, which is one pass under either encoding.
  • The subject of a cursor walk must not change mid-walk. The cursor is honest only over the walk it was started for; re-tie it after replacing the subject.
  • No locales here. Case mapping is per code point (UpperCasePoint and friends); locale-aware collation is a library concern for a later unit, not a hidden behavior of =.

Try it

git clone https://gitlab.com/mica-lang/mica-container.git
make -C mica-container/examples/Text run

Replace the builder loop’s Append with line := line + "ab" and both versions still print the same text — then imagine the loop at a million rounds, and the builder’s reason is the arithmetic. Change a piece’s %ls to %d and the format checker refuses before the build.

Next

UtfSources — the other half of the text story: a source file where identifiers come from any script, and the encoding as a build-time choice the program’s meaning never depends on.