    const std = @import("../std.zig");
    const Allocator = std.mem.Allocator;

    /// This allocator is used in front of another allocator and logs to the provided writer
    /// on every call to the allocator. Writer errors are ignored.
    pub fn LogToWriterAllocator(comptime Writer: type) type {
        return struct {
            parent_allocator: Allocator,
            writer: Writer,

            const Self = @This();

            pub fn init(parent_allocator: Allocator, writer: Writer) Self {
                return Self{
                    .parent_allocator = parent_allocator,
                    .writer = writer,
                };
            }

            pub fn allocator(self: *Self) Allocator {
                return Allocator.init(self, alloc, resize, free);
            }

            fn alloc(
                self: *Self,
                len: usize,
                ptr_align: u29,
                len_align: u29,
                ra: usize,
            ) error{OutOfMemory}![]u8 {
                self.writer.print("alloc : {}", .{len}) catch {};
                const result = self.parent_allocator.rawAlloc(len, ptr_align, len_align, ra);
                if (result) |_| {
                    self.writer.print(" success!\n", .{}) catch {};
                } else |_| {
                    self.writer.print(" failure!\n", .{}) catch {};
                }
                return result;
            }

            fn resize(
                self: *Self,
                buf: []u8,
                buf_align: u29,
                new_len: usize,
                len_align: u29,
                ra: usize,
            ) ?usize {
                if (new_len <= buf.len) {
                    self.writer.print("shrink: {} to {}\n", .{ buf.len, new_len }) catch {};
                } else {
                    self.writer.print("expand: {} to {}", .{ buf.len, new_len }) catch {};
                }

                if (self.parent_allocator.rawResize(buf, buf_align, new_len, len_align, ra)) |resized_len| {
                    if (new_len > buf.len) {
                        self.writer.print(" success!\n", .{}) catch {};
                    }
                    return resized_len;
                }

                std.debug.assert(new_len > buf.len);
                self.writer.print(" failure!\n", .{}) catch {};
                return null;
            }

            fn free(
                self: *Self,
                buf: []u8,
                buf_align: u29,
                ra: usize,
            ) void {
                self.writer.print("free  : {}\n", .{buf.len}) catch {};
                self.parent_allocator.rawFree(buf, buf_align, ra);
            }
        };
    }

    /// This allocator is used in front of another allocator and logs to the provided writer
    /// on every call to the allocator. Writer errors are ignored.
    pub fn logToWriterAllocator(
        parent_allocator: Allocator,
        writer: anytype,
    ) LogToWriterAllocator(@TypeOf(writer)) {
        return LogToWriterAllocator(@TypeOf(writer)).init(parent_allocator, writer);
    }

    test "LogToWriterAllocator" {
        var log_buf: [255]u8 = undefined;
        var fbs = std.io.fixedBufferStream(&log_buf);

        var allocator_buf: [10]u8 = undefined;
        var fixedBufferAllocator = std.mem.validationWrap(std.heap.FixedBufferAllocator.init(&allocator_buf));
        var allocator_state = logToWriterAllocator(fixedBufferAllocator.allocator(), fbs.writer());
        const allocator = allocator_state.allocator();

        var a = try allocator.alloc(u8, 10);
        a = allocator.shrink(a, 5);
        try std.testing.expect(a.len == 5);
        try std.testing.expect(allocator.resize(a, 20) == null);
        allocator.free(a);

        try std.testing.expectEqualSlices(u8,
            \\alloc : 10 success!

            \\shrink: 10 to 5

            \\expand: 5 to 20 failure!

            \\free  : 5

            \\

        , fbs.getWritten());
    }