Documentation pass

Powered by Claude 3.5 Sonnet, and also a little bit of my own
for the `SmartString.eql` function.
This commit is contained in:
Lyssieth 2024-12-07 04:24:09 +02:00
parent d7c39e076a
commit 54a060d4ca
Signed by untrusted user who does not match committer: lyssieth
GPG key ID: 200268854934CFAB

View file

@ -1,9 +1,38 @@
const std = @import("std"); const std = @import("std");
/// SmartString is a memory-aware string type that provides explicit tracking of allocation state.
/// It maintains information about whether the underlying string data is:
/// - Allocated: Owned by an allocator and must be freed
/// - Constant: Compile-time constant that requires no freeing
/// - Dead: Already freed (helps catch use-after-free in debug builds)
///
/// This type is particularly useful in scenarios where string ownership needs to be
/// explicit, such as logging systems or string caches where mixing allocated and
/// constant strings is common.
///
/// Example:
/// ```
/// const str1 = try SmartString.alloc("hello", allocator);
/// defer str1.deinit();
///
/// const str2 = SmartString.constant("world");
/// defer str2.deinit(); // Safe to call deinit() even on constants
/// ```
pub const SmartString = struct { pub const SmartString = struct {
/// The actual string data. This becomes undefined after calling `deinit()`.
data: []const u8, data: []const u8,
/// Tracks the allocation state of the string.
kind: AllocKind, kind: AllocKind,
/// Creates a new SmartString by allocating and copying the input string.
///
/// The resulting string must be freed with `deinit()`.
///
/// Example:
/// ```
/// const str = try SmartString.alloc("dynamic string", allocator);
/// defer str.deinit();
/// ```
pub fn alloc(value: []const u8, allocator: std.mem.Allocator) !SmartString { pub fn alloc(value: []const u8, allocator: std.mem.Allocator) !SmartString {
return .{ return .{
.data = try allocator.dupe(u8, value), .data = try allocator.dupe(u8, value),
@ -11,6 +40,16 @@ pub const SmartString = struct {
}; };
} }
/// Creates a new SmartString from a compile-time known string.
///
/// The string data is stored in the binary and never freed. Calling `deinit()`
/// is still valid and will mark the string as Dead, including swapping out the
/// `data` field for `undefined`.
///
/// Example:
/// ```
/// const str = SmartString.constant("static string");
/// ```
pub fn constant(comptime value: []const u8) SmartString { pub fn constant(comptime value: []const u8) SmartString {
return .{ return .{
.data = value, .data = value,
@ -18,7 +57,16 @@ pub const SmartString = struct {
}; };
} }
/// See also: `cloneAlloc` /// Creates a copy of an allocated SmartString using its original allocator.
///
/// Returns error.NotAllocated if called on a constant or dead string.
/// For those cases, use `cloneAlloc()` instead.
///
/// Example:
/// ```
/// const str1 = try SmartString.alloc("hello", allocator);
/// const str2 = try str1.clone(); // Uses same allocator as str1
/// ```
pub fn clone(self: SmartString) !SmartString { pub fn clone(self: SmartString) !SmartString {
if (!self.kind.isAllocated()) if (!self.kind.isAllocated())
return error.NotAllocated; // should use `cloneAlloc` instead return error.NotAllocated; // should use `cloneAlloc` instead
@ -26,11 +74,61 @@ pub const SmartString = struct {
return try SmartString.alloc(self.data, self.kind.Allocated); return try SmartString.alloc(self.data, self.kind.Allocated);
} }
/// Clones the string to a new allocator. /// Creates a copy of any SmartString using the provided allocator.
///
/// This works for all SmartString variants (allocated, constant, or dead),
/// making it more flexible than `clone()`.
///
/// Example:
/// ```
/// const str1 = SmartString.constant("hello");
/// const str2 = try str1.cloneAlloc(new_allocator);
/// ```
pub fn cloneAlloc(self: SmartString, allocator: std.mem.Allocator) !SmartString { pub fn cloneAlloc(self: SmartString, allocator: std.mem.Allocator) !SmartString {
return try SmartString.alloc(self.data, allocator); return try SmartString.alloc(self.data, allocator);
} }
/// Compares two SmartStrings for equality.
/// Two SmartStrings are equal if they have the same contents.
///
/// It can also compare a SmartString to `[]u8` or `[]const u8`, which will
/// compare the data slices in memory.
///
/// Example:
/// ```
/// const str1 = SmartString.constant("hello");
/// const str2 = SmartString.constant("hello");
/// const str3 = SmartString.constant("world");
///
/// try t.expect(str1.eql(str2));
/// try t.expect(!str1.eql(str3));
/// ```
pub fn eql(self: SmartString, other: anytype) bool {
if (@TypeOf(other) == []const u8) {
return std.mem.eql(u8, self.data, other);
} else if (@TypeOf(other) == []u8) {
return std.mem.eql(u8, self.data, other);
} else if (@TypeOf(other) == SmartString) {
return std.mem.eql(u8, self.data, other.data);
}
}
/// Frees the string if it was allocated and marks it as Dead.
///
/// Safe to call on any variant (allocated, constant, or dead).
/// Will trigger a panic in debug builds if called on an already dead string.
///
/// After calling `deinit()`:
/// - The `data` slice becomes undefined
/// - The `kind` becomes Dead
/// - The string should not be used anymore
///
/// Example:
/// ```
/// var str = try SmartString.alloc("hello", allocator);
/// str.deinit();
/// // str.data is now undefined
/// ```
pub fn deinit(self: *SmartString) void { pub fn deinit(self: *SmartString) void {
switch (self.*.kind) { switch (self.*.kind) {
.Constant => { .Constant => {
@ -53,11 +151,18 @@ pub const SmartString = struct {
} }
}; };
/// Represents the allocation state of a SmartString.
/// This union tracks whether a string is constant, allocated (and by which allocator),
/// or has been freed (dead).
pub const AllocKind = union(enum) { pub const AllocKind = union(enum) {
/// Represents a compile-time constant string that never needs to be freed
Constant: void, Constant: void,
/// Represents an allocated string, storing the allocator that owns it
Allocated: std.mem.Allocator, Allocated: std.mem.Allocator,
/// Represents a string that has been freed and should not be used
Dead: void, Dead: void,
/// Returns true if the string has been freed (is Dead)
inline fn isDead(self: AllocKind) bool { inline fn isDead(self: AllocKind) bool {
return switch (self) { return switch (self) {
.Dead => true, .Dead => true,
@ -65,6 +170,7 @@ pub const AllocKind = union(enum) {
}; };
} }
/// Returns true if the string is currently allocated
inline fn isAllocated(self: AllocKind) bool { inline fn isAllocated(self: AllocKind) bool {
return switch (self) { return switch (self) {
.Allocated => true, .Allocated => true,
@ -72,6 +178,7 @@ pub const AllocKind = union(enum) {
}; };
} }
/// Returns true if the string is a compile-time constant
inline fn isConstant(self: AllocKind) bool { inline fn isConstant(self: AllocKind) bool {
return switch (self) { return switch (self) {
.Constant => true, .Constant => true,
@ -79,6 +186,8 @@ pub const AllocKind = union(enum) {
}; };
} }
/// Compares two AllocKinds for equality
/// Two allocated strings are equal only if they use the same allocator
fn eql(self: AllocKind, other: AllocKind) bool { fn eql(self: AllocKind, other: AllocKind) bool {
if (@as(std.meta.Tag(AllocKind), self) != @as(std.meta.Tag(AllocKind), other)) if (@as(std.meta.Tag(AllocKind), self) != @as(std.meta.Tag(AllocKind), other))
return false; return false;