# Building Dynamic Strings

### Building Dynamic Strings

A dynamic string is text whose length is not fixed ahead of time.

In Zig, dynamic strings are usually built as dynamic byte arrays. The most common tool is:

```zig
std.ArrayList(u8)
```

This means a growable list of bytes.

Since Zig represents text as UTF-8 bytes, a growable list of `u8` values works well for building strings.

#### Why Dynamic Strings Need Memory

A fixed array has a fixed size:

```zig
var buffer: [16]u8 = undefined;
```

This buffer can hold at most 16 bytes.

That is fine when you know the maximum size. But many strings are not known ahead of time.

Examples:

```text
user input
file paths
generated code
JSON output
error messages
HTML pages
SQL queries
HTTP responses
```

For these cases, the string needs room to grow.

#### Using `ArrayList(u8)`

Here is a small example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.appendSlice("Hello");
    try text.append(' ');
    try text.appendSlice("Zig");

    std.debug.print("{s}\n", .{text.items});
}
```

Output:

```text
Hello Zig
```

The list owns heap memory. That is why it needs an allocator.

This line creates the list:

```zig
var text = std.ArrayList(u8).init(allocator);
```

This line frees the memory when the function exits:

```zig
defer text.deinit();
```

The current string contents are stored in:

```zig
text.items
```

That field is a slice:

```zig
[]u8
```

You can print it with `{s}`.

#### Appending One Byte

Use `append` for one byte:

```zig
try text.append('!');
```

The byte `'!'` is added to the end.

Full example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.append('A');
    try text.append('B');
    try text.append('C');

    std.debug.print("{s}\n", .{text.items});
}
```

Output:

```text
ABC
```

This works because ASCII characters are single bytes.

#### Appending a String

Use `appendSlice` for a byte slice:

```zig
try text.appendSlice("hello");
```

A string literal can be used as `[]const u8`, so it can be appended to an `ArrayList(u8)`.

Example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var path = std.ArrayList(u8).init(allocator);
    defer path.deinit();

    try path.appendSlice("/usr");
    try path.append('/');
    try path.appendSlice("local");
    try path.append('/');
    try path.appendSlice("bin");

    std.debug.print("{s}\n", .{path.items});
}
```

Output:

```text
/usr/local/bin
```

#### Formatting into a Dynamic String

`ArrayList` can act like a writer.

This is useful when you want to append formatted values.

```zig
try text.writer().print("x = {}, y = {}", .{ 10, 20 });
```

Complete example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.writer().print("name = {s}, score = {}", .{ "Ada", 95 });

    std.debug.print("{s}\n", .{text.items});
}
```

Output:

```text
name = Ada, score = 95
```

This avoids manual number-to-string conversion.

#### Building Text in Several Steps

Dynamic strings are useful when output is built step by step.

```zig
const std = @import("std");

fn writeUser(text: *std.ArrayList(u8), name: []const u8, age: u32) !void {
    try text.writer().print("user {{ name = {s}, age = {} }}", .{ name, age });
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.appendSlice("record: ");
    try writeUser(&text, "Ada", 36);

    std.debug.print("{s}\n", .{text.items});
}
```

Output:

```text
record: user { name = Ada, age = 36 }
```

The function receives a pointer to the `ArrayList` so it can append to the same list.

```zig
fn writeUser(text: *std.ArrayList(u8), name: []const u8, age: u32) !void
```

This pattern is common when many helper functions contribute to one output buffer.

#### Reserving Capacity

An `ArrayList` grows when needed. Growing may allocate a new buffer and copy the old bytes.

If you have a rough size estimate, you can reserve memory first.

```zig
try text.ensureTotalCapacity(1024);
```

This asks the list to prepare enough space for 1024 bytes.

Example:

```zig
const std = @import("std");

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    var text = std.ArrayList(u8).init(allocator);
    defer text.deinit();

    try text.ensureTotalCapacity(128);

    try text.appendSlice("This text can grow without immediate reallocation.");

    std.debug.print("{s}\n", .{text.items});
}
```

Reserving capacity is not required for correctness. It is a performance tool.

Use it when you expect many appends and can estimate the final size.

#### Length and Capacity

An `ArrayList` has two important ideas:

```text
length:   how many bytes are currently used
capacity: how many bytes can fit before growing again
```

The used bytes are:

```zig
text.items
```

The length is:

```zig
text.items.len
```

The capacity is internal storage size. It may be larger than the length.

For example, a list may have length 11 but capacity 32. Only the first 11 bytes are part of the string.

Always print or use `text.items`, not the whole internal capacity.

#### Clearing and Reusing a List

You can clear a list while keeping its allocated capacity.

```zig
text.clearRetainingCapacity();
```

This sets the length to zero but keeps the memory for reuse.

Example:

```zig
try text.appendSlice("first");
text.clearRetainingCapacity();
try text.appendSlice("second");
```

Now `text.items` contains:

```text
second
```

This is useful in loops where you build many temporary strings.

There is also:

```zig
text.clearAndFree();
```

This clears the list and frees its allocated memory.

Use `clearRetainingCapacity` when you plan to reuse the list. Use `clearAndFree` when you want to release memory.

#### Returning a Dynamic String

Sometimes a function should build a string and return it.

Use `toOwnedSlice`.

```zig
const std = @import("std");

fn makeGreeting(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
    var text = std.ArrayList(u8).init(allocator);
    errdefer text.deinit();

    try text.writer().print("Hello, {s}!", .{name});

    return try text.toOwnedSlice();
}
```

The returned slice is owned by the caller.

Usage:

```zig
pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const greeting = try makeGreeting(allocator, "Zig");
    defer allocator.free(greeting);

    std.debug.print("{s}\n", .{greeting});
}
```

Output:

```text
Hello, Zig!
```

The important rule:

```text
toOwnedSlice transfers ownership to the caller
```

After that, the caller must free the returned slice.

#### Why `errdefer` Is Used

Look at this function again:

```zig
fn makeGreeting(allocator: std.mem.Allocator, name: []const u8) ![]u8 {
    var text = std.ArrayList(u8).init(allocator);
    errdefer text.deinit();

    try text.writer().print("Hello, {s}!", .{name});

    return try text.toOwnedSlice();
}
```

The line:

```zig
errdefer text.deinit();
```

means: if this function returns an error, clean up the list.

This matters because `print` or `toOwnedSlice` may fail if allocation fails.

If everything succeeds, `toOwnedSlice` transfers ownership, so `text.deinit()` should not run.

That is why this uses `errdefer`, not `defer`.

#### Borrowing vs Returning

When a function receives an `ArrayList` pointer, it borrows the list:

```zig
fn appendName(text: *std.ArrayList(u8), name: []const u8) !void {
    try text.appendSlice(name);
}
```

The caller still owns the list.

When a function returns `[]u8` from `toOwnedSlice`, it transfers ownership:

```zig
fn makeName(allocator: std.mem.Allocator) ![]u8 {
    var text = std.ArrayList(u8).init(allocator);
    errdefer text.deinit();

    try text.appendSlice("Zig");
    return try text.toOwnedSlice();
}
```

The caller owns the returned memory.

These two designs are both common. Choose based on who should own the final string.

#### Common Mistake: Forgetting `deinit`

This leaks memory:

```zig
var text = std.ArrayList(u8).init(allocator);
try text.appendSlice("hello");
```

The list allocated memory, but nothing freed it.

Correct:

```zig
var text = std.ArrayList(u8).init(allocator);
defer text.deinit();

try text.appendSlice("hello");
```

Every owning dynamic structure needs a cleanup path.

#### Common Mistake: Keeping `items` Too Long

This can be unsafe:

```zig
const old_items = text.items;

try text.appendSlice("more");

// old_items may no longer be valid
```

Appending may cause the list to reallocate. If that happens, the old storage is freed or replaced.

After changing the list, use `text.items` again.

```zig
try text.appendSlice("more");
const current_items = text.items;
```

The rule is:

```text
do not keep slices into a growable list across operations that may reallocate
```

#### Common Mistake: Using Dynamic Strings When a Buffer Is Enough

Do not allocate when a fixed buffer is enough.

Good for small, bounded output:

```zig
var buffer: [128]u8 = undefined;
const message = try std.fmt.bufPrint(buffer[0..], "id={}", .{42});
```

Good for output that grows unpredictably:

```zig
var text = std.ArrayList(u8).init(allocator);
defer text.deinit();
```

Use the simpler memory model when possible.

A fixed buffer avoids heap allocation. An `ArrayList` gives flexibility.

#### Complete Example

```zig
const std = @import("std");

fn appendCsvField(out: *std.ArrayList(u8), field: []const u8) !void {
    try out.append('"');

    for (field) |byte| {
        if (byte == '"') {
            try out.appendSlice("\"\"");
        } else {
            try out.append(byte);
        }
    }

    try out.append('"');
}

fn makeCsvLine(allocator: std.mem.Allocator, name: []const u8, city: []const u8) ![]u8 {
    var out = std.ArrayList(u8).init(allocator);
    errdefer out.deinit();

    try appendCsvField(&out, name);
    try out.append(',');
    try appendCsvField(&out, city);
    try out.append('\n');

    return try out.toOwnedSlice();
}

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    const line = try makeCsvLine(allocator, "Ada", "London");
    defer allocator.free(line);

    std.debug.print("{s}", .{line});
}
```

Output:

```text
"Ada","London"
```

This example shows a realistic dynamic string builder:

```text
helper functions append into one ArrayList
the final string is returned with toOwnedSlice
the caller frees the returned slice
```

#### Summary

Zig builds dynamic strings with dynamic byte storage.

Use:

```zig
std.ArrayList(u8)
```

when text needs to grow.

Use `append` for one byte, `appendSlice` for text, and `writer().print` for formatted output.

Call `deinit` when the list owns memory. Use `toOwnedSlice` when returning the final string to the caller.

The main rule is ownership: whoever owns the dynamic string must free it.

