ajhahn.de
← Flash
Flash 237 lines
// list — the dynamic array, in pure Flash.
//
// `List(T)` mirrors Zig's unmanaged ArrayList surface — `.empty`, `deinit`,
// `append`, `appendSlice`, `toOwnedSlice`, `.items` — so a facade can
// re-route between the two implementations without touching a call site.
// The allocation is carried as a second slice (`buf`) rather than a raw
// capacity count, so the implementation needs nothing beyond the language's
// slice surface. Growth doubles the capacity (floor 4) and jumps straight
// to the requested length when a bulk append outruns the double.

use "base"
use "mem"

pub fn List(comptime T type) type {
    return struct {
        // The live elements, always a prefix of `buf`; the length is the
        // list length. Index, slice, and write through freely.
        items []mut T,
        // The full allocation. `items.len <= buf.len` holds at all times.
        buf []mut T,

        const Self = #This()

        // The canonical initializer: no allocation until the first append.
        pub const empty Self = .{ .items = &.{}, .buf = &.{} }

        // Release the allocation. The list is `.empty` afterwards and may
        // be reused.
        pub fn deinit(self *mut Self, alloc base.Allocator) void {
            alloc.free(self.buf)
            self.* = .empty
        }

        // Append one element, growing if needed.
        pub fn append(self *mut Self, alloc base.Allocator, item T) !void {
            try self.grow(alloc, self.items.len + 1)
            n := self.items.len
            self.buf[n] = item
            self.items = self.buf[0 .. n + 1]
        }

        // Append every element of `source`, growing at most once.
        pub fn appendSlice(self *mut Self, alloc base.Allocator, source []T) !void {
            try self.grow(alloc, self.items.len + source.len)
            n := self.items.len
            mem.copy(T, self.buf[n .. n + source.len], source)
            self.items = self.buf[0 .. n + source.len]
        }

        // Hand the elements to the caller as an exactly-sized allocation
        // the caller owns. The list is `.empty` afterwards and may be
        // reused.
        pub fn toOwnedSlice(self *mut Self, alloc base.Allocator) ![]mut T {
            owned := try alloc.alloc(T, self.items.len)
            mem.copy(T, owned, self.items)
            alloc.free(self.buf)
            self.* = .empty
            return owned
        }

        // Remove and return the element at `index` in O(1) by moving the
        // last element into its place. Order is not preserved.
        pub fn swapRemove(self *mut Self, index usize) T {
            n := self.items.len
            removed := self.items[index]
            self.items[index] = self.items[n - 1]
            self.items = self.buf[0 .. n - 1]
            return removed
        }

        // Ensure the allocation holds at least `needed` elements. On
        // failure the list is untouched — the old allocation is released
        // only after the new one exists.
        fn grow(self *mut Self, alloc base.Allocator, needed usize) !void {
            if needed <= self.buf.len {
                return
            }
            var cap usize = self.buf.len * 2
            if cap < 4 {
                cap = 4
            }
            if cap < needed {
                cap = needed
            }
            grown := try alloc.alloc(T, cap)
            mem.copy(T, grown, self.items)
            alloc.free(self.buf)
            self.buf = grown
            self.items = grown[0..self.items.len]
        }
    }
}

const Pair = struct {
    key i32,
    tag u8,
}

test "append grows past the initial capacity" {
    var xs List(i32) = .empty
    defer xs.deinit(base.testAlloc)
    var v i32 = 0
    while v < 9 {
        try xs.append(base.testAlloc, v)
        v += 1
    }
    try base.expectEqual(9, xs.items.len)
    try base.expectEqual(0, xs.items[0])
    try base.expectEqual(8, xs.items[8])
    try base.expect(xs.buf.len >= 9)
}

test "appendSlice appends in bulk" {
    var s List(u8) = .empty
    defer s.deinit(base.testAlloc)
    try s.appendSlice(base.testAlloc, "flash")
    try s.append(base.testAlloc, '!')
    try base.expect(mem.eql(u8, s.items, "flash!"))
}

test "toOwnedSlice hands off the elements and resets the list" {
    var s List(u8) = .empty
    defer s.deinit(base.testAlloc)
    try s.appendSlice(base.testAlloc, "ab")
    owned := try s.toOwnedSlice(base.testAlloc)
    defer base.testAlloc.free(owned)
    try base.expect(mem.eql(u8, owned, "ab"))
    try base.expectEqual(0, s.items.len)
    try s.append(base.testAlloc, 'c')
    try base.expectEqual('c', s.items[0])
}

test "toOwnedSlice on an empty list returns an empty slice" {
    var s List(u8) = .empty
    owned := try s.toOwnedSlice(base.testAlloc)
    defer base.testAlloc.free(owned)
    try base.expectEqual(0, owned.len)
}

test "swapRemove takes from the middle and the end" {
    var xs List(u8) = .empty
    defer xs.deinit(base.testAlloc)
    try xs.appendSlice(base.testAlloc, "abcd")
    try base.expectEqual('b', xs.swapRemove(1))
    try base.expect(mem.eql(u8, xs.items, "adc"))
    try base.expectEqual('c', xs.swapRemove(2))
    try base.expect(mem.eql(u8, xs.items, "ad"))
    try base.expectEqual('a', xs.swapRemove(0))
    try base.expectEqual('d', xs.swapRemove(0))
    try base.expectEqual(0, xs.items.len)
}

test "deinit releases and the list is reusable" {
    var s List(u8) = .empty
    try s.append(base.testAlloc, 'x')
    s.deinit(base.testAlloc)
    try base.expectEqual(0, s.items.len)
    try s.append(base.testAlloc, 'y')
    try base.expectEqual('y', s.items[0])
    s.deinit(base.testAlloc)
}

// The failing-allocator sweep bodies. `checkAllAllocationFailures` re-runs
// each one, failing a different allocation per run: every induced OOM must
// propagate as an error with nothing leaked, and the final unfailed run
// must pass the assertions.
fn growthSweep(alloc base.Allocator) !void {
    var xs List(i32) = .empty
    defer xs.deinit(alloc)
    var v i32 = 0
    while v < 40 {
        try xs.append(alloc, v)
        v += 1
    }
    try base.expectEqual(40, xs.items.len)
}

fn appendSliceSweep(alloc base.Allocator) !void {
    var s List(u8) = .empty
    defer s.deinit(alloc)
    try s.appendSlice(alloc, "ab")
    try s.appendSlice(alloc, "0123456789abcdef")
    try base.expect(mem.eql(u8, s.items, "ab0123456789abcdef"))
}

fn toOwnedSliceSweep(alloc base.Allocator) !void {
    var s List(u8) = .empty
    defer s.deinit(alloc)
    try s.appendSlice(alloc, "flash")
    owned := try s.toOwnedSlice(alloc)
    defer alloc.free(owned)
    try base.expect(mem.eql(u8, owned, "flash"))
}

test "append survives failure at every allocation point" {
    try base.checkAllAllocationFailures(base.testAlloc, growthSweep, .{})
}

test "appendSlice survives failure at every allocation point" {
    try base.checkAllAllocationFailures(base.testAlloc, appendSliceSweep, .{})
}

test "toOwnedSlice survives failure at every allocation point" {
    try base.checkAllAllocationFailures(base.testAlloc, toOwnedSliceSweep, .{})
}

test "a failed grow leaves the list untouched" {
    var failing = base.FailingAllocator.init(base.testAlloc, .{ .fail_index = 1 })
    fa := failing.allocator()
    var xs List(u8) = .empty
    defer xs.deinit(base.testAlloc)
    try xs.appendSlice(fa, "abcd")
    try base.expectError(error.OutOfMemory, xs.append(fa, 'e'))
    try base.expect(mem.eql(u8, xs.items, "abcd"))
    try base.expectEqual(4, xs.buf.len)
    try xs.append(base.testAlloc, 'e')
    try base.expect(mem.eql(u8, xs.items, "abcde"))
}

test "struct and optional element types instantiate" {
    var ps List(Pair) = .empty
    defer ps.deinit(base.testAlloc)
    try ps.append(base.testAlloc, .{ .key = 1, .tag = 'a' })
    try ps.append(base.testAlloc, .{ .key = 2, .tag = 'b' })
    try base.expectEqual(2, ps.items.len)
    try base.expectEqual('b', ps.items[1].tag)
    ps.items[0].key = 9
    try base.expectEqual(9, ps.items[0].key)

    var os List(?[]u8) = .empty
    defer os.deinit(base.testAlloc)
    try os.append(base.testAlloc, null)
    try os.append(base.testAlloc, "s")
    try base.expect(os.items[0] == null)
    try base.expect(os.items[1] != null)
}