Adjust naming schemes

Top-level struct OP
This commit is contained in:
Lyssieth 2025-05-03 21:42:28 +03:00
parent 68b73910da
commit ba12ce082d
Signed by untrusted user who does not match committer: lyssieth
GPG key ID: 200268854934CFAB
3 changed files with 252 additions and 254 deletions

251
src/util/SmartString.zig Normal file
View file

@ -0,0 +1,251 @@
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
/// ```
/// The actual string data. This becomes undefined after calling `deinit()`.
data: []const u8,
/// Tracks the allocation state of the string.
kind: AllocKind,
const SmartString = @This();
/// 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 {
return .{
.data = try allocator.dupe(u8, value),
.kind = .{ .Allocated = allocator },
};
}
/// 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 {
return .{
.data = value,
.kind = .{ .Constant = {} },
};
}
/// 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 {
if (!self.kind.isAllocated())
return error.NotAllocated; // should use `cloneAlloc` instead
return try SmartString.alloc(self.data, self.kind.Allocated);
}
/// 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 {
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 {
switch (self.*.kind) {
.Constant => {
self.*.data = undefined;
self.*.kind = .{ .Dead = {} };
},
.Allocated => |allocator| {
allocator.free(self.data);
self.*.data = undefined;
self.*.kind = .{ .Dead = {} };
},
.Dead => {
if (std.debug.runtime_safety) {
std.debug.panic("Double free of SmartString", .{});
}
},
}
}
/// 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) {
/// Represents a compile-time constant string that never needs to be freed
Constant: void,
/// Represents an allocated string, storing the allocator that owns it
Allocated: std.mem.Allocator,
/// Represents a string that has been freed and should not be used
Dead: void,
/// Returns true if the string has been freed (is Dead)
inline fn isDead(self: AllocKind) bool {
return switch (self) {
.Dead => true,
else => false,
};
}
/// Returns true if the string is currently allocated
inline fn isAllocated(self: AllocKind) bool {
return switch (self) {
.Allocated => true,
else => false,
};
}
/// Returns true if the string is a compile-time constant
inline fn isConstant(self: AllocKind) bool {
return switch (self) {
.Constant => true,
else => false,
};
}
/// Compares two AllocKinds for equality
/// Two allocated strings are equal only if they use the same allocator
fn eql(self: AllocKind, other: AllocKind) bool {
if (@as(std.meta.Tag(AllocKind), self) != @as(std.meta.Tag(AllocKind), other))
return false;
return switch (self) {
.Allocated => |a| a.ptr == other.Allocated.ptr,
else => true,
};
}
};
const t = std.testing;
test "the different kinds work" {
const a = t.allocator;
var strOne = try SmartString.alloc("hello, world", a);
defer strOne.deinit();
try t.expectEqualStrings("hello, world", strOne.data);
try t.expectEqual(AllocKind{ .Allocated = a }, strOne.kind);
var strTwo = SmartString.constant("hello, world");
defer strTwo.deinit();
try t.expectEqualStrings("hello, world", strTwo.data);
try t.expectEqual(AllocKind{ .Constant = {} }, strTwo.kind);
try t.expectEqualStrings(strOne.data, strTwo.data);
try t.expect(!strOne.kind.eql(strTwo.kind));
}
test "allocKind eql works" {
const a = AllocKind{ .Dead = {} };
const b = AllocKind{ .Constant = {} };
try t.expect(!a.eql(b));
try t.expect(!b.eql(a));
const c = AllocKind{ .Allocated = t.allocator };
const d = AllocKind{ .Allocated = t.allocator };
try t.expect(c.eql(d));
try t.expect(!d.eql(a));
try t.expect(!d.eql(b));
}
test "clone works" {
const a = t.allocator;
var strOne = try SmartString.alloc("hello, world", a);
defer strOne.deinit();
var strTwo = try strOne.clone();
defer strTwo.deinit();
try t.expectEqualStrings("hello, world", strOne.data);
try t.expectEqualStrings("hello, world", strTwo.data);
try t.expect(strOne.kind.eql(strTwo.kind));
}

View file

@ -1,251 +0,0 @@
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 {
/// The actual string data. This becomes undefined after calling `deinit()`.
data: []const u8,
/// Tracks the allocation state of the string.
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 {
return .{
.data = try allocator.dupe(u8, value),
.kind = .{ .Allocated = allocator },
};
}
/// 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 {
return .{
.data = value,
.kind = .{ .Constant = {} },
};
}
/// 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 {
if (!self.kind.isAllocated())
return error.NotAllocated; // should use `cloneAlloc` instead
return try SmartString.alloc(self.data, self.kind.Allocated);
}
/// 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 {
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 {
switch (self.*.kind) {
.Constant => {
self.*.data = undefined;
self.*.kind = .{ .Dead = {} };
},
.Allocated => |allocator| {
allocator.free(self.data);
self.*.data = undefined;
self.*.kind = .{ .Dead = {} };
},
.Dead => {
if (std.debug.runtime_safety) {
std.debug.panic("Double free of SmartString", .{});
}
},
}
}
};
/// 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) {
/// Represents a compile-time constant string that never needs to be freed
Constant: void,
/// Represents an allocated string, storing the allocator that owns it
Allocated: std.mem.Allocator,
/// Represents a string that has been freed and should not be used
Dead: void,
/// Returns true if the string has been freed (is Dead)
inline fn isDead(self: AllocKind) bool {
return switch (self) {
.Dead => true,
else => false,
};
}
/// Returns true if the string is currently allocated
inline fn isAllocated(self: AllocKind) bool {
return switch (self) {
.Allocated => true,
else => false,
};
}
/// Returns true if the string is a compile-time constant
inline fn isConstant(self: AllocKind) bool {
return switch (self) {
.Constant => true,
else => false,
};
}
/// Compares two AllocKinds for equality
/// Two allocated strings are equal only if they use the same allocator
fn eql(self: AllocKind, other: AllocKind) bool {
if (@as(std.meta.Tag(AllocKind), self) != @as(std.meta.Tag(AllocKind), other))
return false;
return switch (self) {
.Allocated => |a| a.ptr == other.Allocated.ptr,
else => true,
};
}
};
const t = std.testing;
test "the different kinds work" {
const a = t.allocator;
var strOne = try SmartString.alloc("hello, world", a);
defer strOne.deinit();
try t.expectEqualStrings("hello, world", strOne.data);
try t.expectEqual(AllocKind{ .Allocated = a }, strOne.kind);
var strTwo = SmartString.constant("hello, world");
defer strTwo.deinit();
try t.expectEqualStrings("hello, world", strTwo.data);
try t.expectEqual(AllocKind{ .Constant = {} }, strTwo.kind);
try t.expectEqualStrings(strOne.data, strTwo.data);
try t.expect(!strOne.kind.eql(strTwo.kind));
}
test "allocKind eql works" {
const a = AllocKind{ .Dead = {} };
const b = AllocKind{ .Constant = {} };
try t.expect(!a.eql(b));
try t.expect(!b.eql(a));
const c = AllocKind{ .Allocated = t.allocator };
const d = AllocKind{ .Allocated = t.allocator };
try t.expect(c.eql(d));
try t.expect(!d.eql(a));
try t.expect(!d.eql(b));
}
test "clone works" {
const a = t.allocator;
var strOne = try SmartString.alloc("hello, world", a);
defer strOne.deinit();
var strTwo = try strOne.clone();
defer strTwo.deinit();
try t.expectEqualStrings("hello, world", strOne.data);
try t.expectEqualStrings("hello, world", strTwo.data);
try t.expect(strOne.kind.eql(strTwo.kind));
}

View file

@ -1,5 +1,4 @@
const str = @import("./smartString.zig");
pub const SmartString = str.SmartString;
pub const SmartString = @import("./SmartString.zig");
const queue = @import("./queue.zig");
pub const Queue = queue.MPSCQueue;
@ -11,7 +10,6 @@ comptime {
const builtin = @import("builtin");
if (builtin.is_test) {
std.mem.doNotOptimizeAway(str);
std.mem.doNotOptimizeAway(SmartString);
std.mem.doNotOptimizeAway(queue);