Add Leak Detection to Ona Heap Allocator #15

Merged
kayomn merged 12 commits from ona-allocator-safety-tracker into main 2023-06-04 16:07:47 +02:00
9 changed files with 410 additions and 160 deletions

View File

@ -49,7 +49,7 @@ pub const Stacking = struct {
} }
fn allocate_page(self: *Stacking, page_size: usize) io.AllocationError!*Page { fn allocate_page(self: *Stacking, page_size: usize) io.AllocationError!*Page {
var buffer = try io.allocate_many(u8, page_size, self.base_allocator); var buffer = try io.allocate_many(self.base_allocator, page_size, u8);
errdefer io.deallocate(self.base_allocator, buffer); errdefer io.deallocate(self.base_allocator, buffer);

View File

@ -7,6 +7,7 @@ pub const AllocationError = error {
}; };
pub const AllocationOptions = struct { pub const AllocationOptions = struct {
return_address: usize,
allocation: ?[]u8 = null, allocation: ?[]u8 = null,
size: usize, size: usize,
}; };
@ -133,57 +134,17 @@ pub const FixedBuffer = struct {
} }
}; };
pub const GrowingBuffer = struct {
allocator: Allocator,
appender: Appender,
const AppendOptions = struct {
allocator: Allocator,
bytes: []const u8,
};
const Appender = Generator(AllocationError!void, AppendOptions);
pub fn as_writer(self: *GrowingBuffer) Writer {
return Writer.bind(GrowingBuffer, self, struct {
fn write(growing_buffer: *GrowingBuffer, bytes: []const u8) ?usize {
growing_buffer.write(bytes) catch return null;
return bytes.len;
}
}.write);
}
pub fn bind(comptime State: type, allocator: Allocator, state: *State, comptime appender: fn (capture: *State, allocator: Allocator, bytes: []const u8) AllocationError!void) GrowingBuffer {
return .{
.appender = Appender.bind(State, state, struct {
fn append(self: *State, options: AppendOptions) AllocationError!void {
return appender(self, options.allocator, options.bytes);
}
}.append),
.allocator = allocator,
};
}
pub fn write(self: GrowingBuffer, bytes: []const u8) AllocationError!void {
return self.appender.invoke(.{
.allocator = self.allocator,
.bytes = bytes,
});
}
};
pub const Writer = Generator(?usize, []const u8); pub const Writer = Generator(?usize, []const u8);
pub fn allocate_many(comptime Type: type, amount: usize, allocator: Allocator) AllocationError![]Type { pub fn allocate_many(allocator: Allocator, amount: usize, comptime Type: type) AllocationError![]Type {
if (@sizeOf(Type) == 0) { if (@sizeOf(Type) == 0) {
@compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type)); @compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type));
} }
return @ptrCast([*]Type, @alignCast(@alignOf(Type), allocator.invoke(.{.size = @sizeOf(Type) * amount}) orelse { return @ptrCast([*]Type, @alignCast(@alignOf(Type), allocator.invoke(.{
return error.OutOfMemory; .size = @sizeOf(Type) * amount,
}))[0 .. amount]; .return_address = @returnAddress(),
}) orelse return error.OutOfMemory))[0 .. amount];
} }
pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@TypeOf(value) { pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@TypeOf(value) {
@ -193,9 +154,10 @@ pub fn allocate_one(allocator: Allocator, value: anytype) AllocationError!*@Type
@compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type)); @compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type));
} }
const allocation = @ptrCast(*Type, @alignCast(@alignOf(Type), allocator.invoke(.{.size = @sizeOf(Type)}) orelse { const allocation = @ptrCast(*Type, @alignCast(@alignOf(Type), allocator.invoke(.{
return error.OutOfMemory; .size = @sizeOf(Type),
})); .return_address = @returnAddress(),
}) orelse return error.OutOfMemory));
allocation.* = value; allocation.* = value;
@ -232,17 +194,16 @@ pub fn copy(target: []u8, source: []const u8) void {
} }
pub fn deallocate(allocator: Allocator, allocation: anytype) void { pub fn deallocate(allocator: Allocator, allocation: anytype) void {
const Allocation = @TypeOf(allocation); switch (@typeInfo(@TypeOf(allocation))) {
.Pointer => |pointer| {
switch (@typeInfo(Allocation)) {
.Pointer => |allocation_pointer| {
_ = allocator.invoke(.{ _ = allocator.invoke(.{
.allocation = switch (allocation_pointer.size) { .allocation = switch (pointer.size) {
.One => @ptrCast([*]u8, allocation)[0 .. @sizeOf(Allocation)], .One => @ptrCast([*]u8, allocation)[0 .. @sizeOf(pointer.child)],
.Slice => @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(Allocation) * allocation.len)], .Slice => @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(pointer.child) * allocation.len)],
.Many, .C => @compileError("length of allocation must be known to deallocate"), .Many, .C => @compileError("length of allocation must be known to deallocate"),
}, },
.return_address = @returnAddress(),
.size = 0, .size = 0,
}); });
}, },

View File

@ -17,19 +17,6 @@ pub fn Stack(comptime Value: type) type {
/// ///
const Self = @This(); const Self = @This();
///
/// Returns a [io.GrowableBuffer] bound to `self` and `allocator`.
///
/// The returned buffer may be used to write to the stack without needing to explicitly pass an allocator
/// context, as well decay further into a generic [io.Writer] type.
///
/// *Note* if `capacity` is a non-zero value, `allocator` must reference the same allocation strategy as the one
/// originally used to allocate the current internal buffer.
///
pub fn as_buffer(self: *Self, allocator: io.Allocator) io.GrowingBuffer {
return io.GrowingBuffer.bind(Self, allocator, self, push_all);
}
/// ///
/// Clears all elements from `self` while preserving the current internal buffer. /// Clears all elements from `self` while preserving the current internal buffer.
/// ///
@ -52,7 +39,7 @@ pub fn Stack(comptime Value: type) type {
return; return;
} }
io.deallocate(allocator, self.values); io.deallocate(allocator, self.values.ptr[0 .. self.capacity]);
self.values = &.{}; self.values = &.{};
self.capacity = 0; self.capacity = 0;
@ -87,7 +74,7 @@ pub fn Stack(comptime Value: type) type {
/// ///
pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void { pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void {
const grown_capacity = self.capacity + growth_amount; const grown_capacity = self.capacity + growth_amount;
const values = (try io.allocate_many(Value, grown_capacity, allocator))[0 .. self.values.len]; const values = (try io.allocate_many(allocator, grown_capacity, Value))[0 .. self.values.len];
errdefer io.deallocate(allocator, values); errdefer io.deallocate(allocator, values);
@ -96,7 +83,7 @@ pub fn Stack(comptime Value: type) type {
values[index] = self.values[index]; values[index] = self.values[index];
} }
io.deallocate(allocator, self.values); io.deallocate(allocator, self.values.ptr[0 .. self.capacity]);
} }
self.values = values; self.values = values;
@ -132,7 +119,7 @@ pub fn Stack(comptime Value: type) type {
pub fn push_all(self: *Self, allocator: io.Allocator, values: []const Value) io.AllocationError!void { pub fn push_all(self: *Self, allocator: io.Allocator, values: []const Value) io.AllocationError!void {
const new_length = self.values.len + values.len; const new_length = self.values.len + values.len;
if (new_length >= self.capacity) { if (new_length > self.capacity) {
try self.grow(allocator, values.len + values.len); try self.grow(allocator, values.len + values.len);
} }
@ -194,3 +181,49 @@ pub fn Stack(comptime Value: type) type {
} }
}; };
} }
///
/// Bridge context between a list type implement as part of the list module and an allocator, allowing the list resource
/// referenced by the [Writable] instance to be written to directly or virtually via the [io.Writer] interface.
///
/// *Note* if the given list contains an existing allocation, the provided [io.Allocator] instance must reference the
/// same allocation strategy as the one originally used to allocate the list type memory.
///
pub const Writable = struct {
allocator: io.Allocator,
list: union (enum) {
stack: *ByteStack,
},
///
/// Stack of bytes.
///
const ByteStack = Stack(u8);
///
/// Returns a [io.Writer] instance that binds a reference of `self` to the [write] operation.
///
pub fn as_writer(self: *Writable) io.Writer {
return io.Writer.bind(Writable, self, struct {
fn write(writable: *Writable, bytes: []const u8) ?usize {
writable.write(bytes) catch return null;
return bytes.len;
}
}.write);
}
///
/// Attempts to call the appropriate multi-element writing function for the current list referenced by `self`,
/// passing `bytes` along.
///
/// The function returns [io.AllocationError] if `allocator` could not commit the memory by the list implementation
/// referenced by `self`. See the specific implementation details of the respective list type for more information.
///
pub fn write(self: *Writable, bytes: []const u8) io.AllocationError!void {
return switch (self.list) {
.stack => |stack| stack.push_all(self.allocator, bytes),
};
}
};

View File

@ -52,17 +52,6 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
entry.value = value; entry.value = value;
} }
///
/// Fetches the value referenced by `index` in `self`, returning it.
///
pub fn fetch(self: *Self, index: Index) Value {
const entry = &self.table[index];
debug.assert(entry.* == .value);
return entry.value;
}
/// ///
/// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`. /// Deinitializes `self` and sets it to an invalid state, freeing all memory allocated by `allocator`.
/// ///
@ -81,6 +70,17 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
self.free_index = 0; self.free_index = 0;
} }
///
/// Fetches the value referenced by `index` in `self`, returning it.
///
pub fn fetch(self: *Self, index: Index) Value {
const entry = &self.table[index];
debug.assert(entry.* == .value);
return entry.value;
}
/// ///
/// Attempts to grow the internal buffer of `self` by `growth_amount` using `allocator`. /// Attempts to grow the internal buffer of `self` by `growth_amount` using `allocator`.
/// ///
@ -95,7 +95,7 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
/// ///
pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void { pub fn grow(self: *Self, allocator: io.Allocator, growth_amount: usize) io.AllocationError!void {
const grown_capacity = self.table.len + growth_amount; const grown_capacity = self.table.len + growth_amount;
const entries = try io.allocate_many(Entry, grown_capacity, allocator); const entries = try io.allocate_many(allocator, grown_capacity, Entry);
errdefer io.deallocate(allocator, entries); errdefer io.deallocate(allocator, entries);
@ -147,12 +147,20 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
debug.assert(entry.* == .free_index); debug.assert(entry.* == .free_index);
self.count += 1;
self.free_index = entry.free_index; self.free_index = entry.free_index;
entry.* = .{.value = value}; entry.* = .{.value = value};
return entry_index; return entry_index;
} }
///
/// Returns `true` if `self` contains no values, otherwise `false`.
///
pub fn is_empty(self: Self) bool {
return self.count == 0;
}
/// ///
/// Removes the value referenced by `index` from `self`. /// Removes the value referenced by `index` from `self`.
/// ///
@ -161,6 +169,7 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type
debug.assert(entry.* == .value); debug.assert(entry.* == .value);
self.count -= 1;
entry.* = .{.free_index = self.free_index}; entry.* = .{.free_index = self.free_index};
self.free_index = index; self.free_index = index;
} }

View File

@ -65,6 +65,30 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
} }
}; };
///
/// Iterable wrapper for [Hashed] instances to make unordered traversal of key-value entries relatively trivial.
///
pub const Iterable = struct {
hashed_map: *Self,
iterations: usize = 0,
///
/// Attempts to move past the current iteration of `self` and onto the next key-value entry, returning it or
/// `null` if there are no more elements in the referenced map.
///
pub fn next(self: *Iterable) ?Entry {
while (self.iterations < self.hashed_map.table.len) {
defer self.iterations += 1;
if (self.hashed_map.table[self.iterations]) |entry| {
return entry;
}
}
return null;
}
};
/// ///
/// Table type. /// Table type.
/// ///
@ -228,7 +252,7 @@ pub fn Hashed(comptime Key: type, comptime Value: type, comptime keyer: Keyer(Ke
pub fn rehash(self: *Self, allocator: io.Allocator, requested_range: usize) io.AllocationError!void { pub fn rehash(self: *Self, allocator: io.Allocator, requested_range: usize) io.AllocationError!void {
const old_table = self.table; const old_table = self.table;
self.table = try io.allocate_many(?Entry, math.max(requested_range, self.count), allocator); self.table = try io.allocate_many(allocator, math.max(requested_range, self.count), ?Entry);
errdefer { errdefer {
io.deallocate(allocator, self.table); io.deallocate(allocator, self.table);

View File

@ -1,54 +1,254 @@
const builtin = @import("builtin");
const coral = @import("coral"); const coral = @import("coral");
const ext = @import("./ext.zig"); const ext = @import("./ext.zig");
const std = @import("std");
///
/// Recorded allocation info state.
///
const AllocationInfo = struct {
trace: AllocationTrace,
next_info: ?*AllocationInfo,
size: usize,
};
///
/// Recorded stack trace of allocation call site.
///
/// *Note* this structure is reduced to zero bytes in released builds optimized for speed or size.
///
const AllocationTrace = std.debug.ConfigurableTrace(2, 4, switch (builtin.mode) {
.Debug, .ReleaseSafe => true,
.ReleaseFast, .ReleaseSmall => false,
});
///
/// Heap allocation context.
///
const Context = struct { const Context = struct {
live_allocations: usize, allocation_info_head: ?*AllocationInfo = null,
const Self = @This(); ///
/// Attempts to allocate a buffer of `size` length from `self`, with `return_address` as the location of the
/// allocation request origin.
///
/// A reference to the allocated buffer is returned via a slice if the allocation was successful, otherwise `null`
/// is returned.
///
/// *Note* the returned buffer must be deallocated with [deallocate] before program exit or it will cause a memory
/// leak.
///
/// *Note* allocation checks are disabled in release builds optimized for speed or size.
///
fn allocate(self: *Context, size: usize, return_address: usize) ?[]u8 {
switch (builtin.mode) {
.Debug, .ReleaseSafe => {
const allocation_info_size = @sizeOf(AllocationInfo);
const total_allocation_size = allocation_info_size + size;
const allocation = ext.SDL_malloc(total_allocation_size) orelse return null;
const allocation_info = @ptrCast(*AllocationInfo, @alignCast(@alignOf(AllocationInfo), allocation));
const empty_allocation = [0]u8{}; allocation_info.* = .{
.size = size,
.next_info = self.allocation_info_head,
.trace = .{},
};
fn reallocate(self: *Self, options: coral.io.AllocationOptions) ?[]u8 { allocation_info.trace.addAddr(return_address, "");
if (options.size == 0) {
if (options.allocation) |allocation| { self.allocation_info_head = allocation_info;
if (allocation.ptr != &empty_allocation) {
return @ptrCast([*]u8, allocation)[allocation_info_size .. total_allocation_size];
},
.ReleaseFast, .ReleaseSmall => {
return @ptrCast([*]u8, ext.SDL_malloc(size) orelse return null)[0 .. size];
},
}
}
///
/// Returns the assumed pointer to the [AllocationInfo] address of `allocation`.
///
fn allocation_info_of(allocation: [*]u8) *AllocationInfo {
return @intToPtr(*AllocationInfo, @ptrToInt(allocation) - @sizeOf(AllocationInfo));
}
///
/// Deallocates a the allocation buffer referenced by `allocation`.
///
/// *Note* the pointer and length of `allocation` must match valid values known to `allocator` otherwise safety-
/// checked behavior will occur.
///
/// *Note* allocation checks are disabled in release builds optimized for speed or size.
///
fn deallocate(self: *Context, allocation: []u8) void {
switch (builtin.mode) {
.Debug, .ReleaseSafe => {
const target_allocation_info = allocation_info_of(allocation.ptr);
if (target_allocation_info.size != allocation.len) {
@panic("incorrect allocation length for deallocating");
}
if (self.allocation_info_head) |allocation_info_head| {
if (target_allocation_info == allocation_info_head) {
self.allocation_info_head = allocation_info_head.next_info;
ext.SDL_free(target_allocation_info);
return;
}
var previous_allocation_info = allocation_info_head;
var current_allocation_info = allocation_info_head.next_info;
while (current_allocation_info) |allocation_info| {
if (allocation_info == target_allocation_info) {
previous_allocation_info.next_info = allocation_info.next_info;
ext.SDL_free(target_allocation_info);
return;
}
previous_allocation_info = allocation_info;
current_allocation_info = allocation_info.next_info;
}
}
@panic("incorrect allocation address for deallocating");
},
.ReleaseFast, .ReleaseSmall => {
ext.SDL_free(allocation.ptr); ext.SDL_free(allocation.ptr);
} },
self.live_allocations -= 1;
return null;
}
self.live_allocations += 1;
return &empty_allocation;
}
if (options.allocation) |allocation| {
if (ext.SDL_realloc(allocation.ptr, options.size)) |reallocation| {
self.live_allocations += 1;
return @ptrCast([*]u8, reallocation)[0 .. options.size];
} }
} }
if (ext.SDL_malloc(options.size)) |allocation| { ///
self.live_allocations += 1; /// Attempts to reallocate the buffer referenced by `allocation` to be `size` length from `self`.
///
/// A reference to the reallocated buffer is returned via a slice if the reallocation was successful, otherwise
/// `null` is returned.
///
/// *Note* the returned buffer must be deallocated with [deallocate] before program exit or it will cause a memory
/// leak.
///
/// *Note* the pointer and length of `allocation` must match valid values known to `allocator` otherwise safety-
/// checked behavior will occur.
///
/// *Note* the allocation referenced by `allocation` should be considered invalid once the function returns,
/// discarding it in favor of the return value.
///
/// *Note* allocation checks are disabled in release builds optimized for speed or size.
///
fn reallocate(self: *Context, allocation: []u8, size: usize) ?[]u8 {
switch (builtin.mode) {
.Debug, .ReleaseSafe => {
const target_allocation_info = allocation_info_of(allocation.ptr);
return @ptrCast([*]u8, allocation)[0 .. options.size]; if (target_allocation_info.size != allocation.len) {
@panic("incorrect allocation length for reallocating");
} }
return null; const allocation_info_size = @sizeOf(AllocationInfo);
if (self.allocation_info_head) |allocation_info_head| {
if (target_allocation_info == allocation_info_head) {
self.allocation_info_head = allocation_info_head.next_info;
const allocation_address = ext.SDL_realloc(target_allocation_info, size) orelse return null;
target_allocation_info.size = size;
return @ptrCast([*]u8, allocation_address)[
allocation_info_size .. (allocation_info_size + size)];
}
var previous_allocation_info = allocation_info_head;
var current_allocation_info = allocation_info_head.next_info;
while (current_allocation_info) |allocation_info| {
if (allocation_info == target_allocation_info) {
previous_allocation_info.next_info = allocation_info.next_info;
const allocation_address = ext.SDL_realloc(target_allocation_info, size) orelse return null;
target_allocation_info.size = size;
return @ptrCast([*]u8, allocation_address)[
allocation_info_size .. (allocation_info_size + size)];
}
previous_allocation_info = allocation_info;
current_allocation_info = allocation_info.next_info;
}
}
@panic("incorrect allocation address for reallocating");
},
.ReleaseFast, .ReleaseSmall => {
return @ptrCast([*]u8, ext.SDL_realloc(allocation.ptr, size) orelse return null)[0 .. size];
},
}
} }
}; };
var context = Context{ ///
.live_allocations = 0, /// Heap context.
}; ///
var context = Context{};
/// ///
/// Heap allocator. /// Heap allocator.
/// ///
pub const allocator = coral.io.Allocator.bind(Context, &context, Context.reallocate); pub const allocator = coral.io.Allocator.bind(Context, &context, struct {
fn reallocate(self: *Context, options: coral.io.AllocationOptions) ?[]u8 {
if (options.size == 0) {
if (options.allocation) |allocation| {
self.deallocate(allocation);
return null;
}
return self.allocate(0, options.return_address);
}
if (options.allocation) |allocation| {
return self.reallocate(allocation, options.size);
}
return self.allocate(options.size, options.return_address);
}
}.reallocate);
///
/// Checks for any allocations belonging to the process heap allocated through the [allocator] interface that are still
/// alive and reports the stack traces of any detected allocations to stderr along with the allocation address and
/// length.
///
/// *Note* this function becomes a no-op in release builds optimized for speed or size.
///
pub fn trace_leaks() void {
switch (builtin.mode) {
.Debug, .ReleaseSafe => {
var current_allocation_info = context.allocation_info_head;
while (current_allocation_info) |allocation_info| : (current_allocation_info = allocation_info.next_info) {
std.debug.print("{d} byte leak at 0x{x} detected:\n", .{
allocation_info.size,
@ptrToInt(allocation_info) + @sizeOf(AllocationInfo),
});
allocation_info.trace.dump();
}
},
.ReleaseFast, .ReleaseSmall => {},
}
}

View File

@ -53,7 +53,7 @@ fn clear_error_details(self: *Self) void {
pub fn compile(self: *Self, data: []const u8) types.RuntimeError!void { pub fn compile(self: *Self, data: []const u8) types.RuntimeError!void {
var ast = try Ast.init(self.env.allocator); var ast = try Ast.init(self.env.allocator);
errdefer ast.deinit(); defer ast.deinit();
{ {
var tokenizer = tokens.Tokenizer{.source = data}; var tokenizer = tokens.Tokenizer{.source = data};
@ -62,9 +62,12 @@ pub fn compile(self: *Self, data: []const u8) types.RuntimeError!void {
if (init_error == error.BadSyntax) { if (init_error == error.BadSyntax) {
self.clear_error_details(); self.clear_error_details();
var message_buffer = self.message_data.as_buffer(self.env.allocator); var writable_data = coral.list.Writable{
.allocator = self.env.allocator,
.list = .{.stack = &self.message_data},
};
coral.utf8.print_formatted(message_buffer.as_writer(), "@({line}): {name}", .{ coral.utf8.print_formatted(writable_data.as_writer(), "@({line}): {name}", .{
.line = tokenizer.lines_stepped, .line = tokenizer.lines_stepped,
.name = ast.error_message, .name = ast.error_message,
}) catch return error.OutOfMemory; }) catch return error.OutOfMemory;

View File

@ -69,21 +69,6 @@ const Object = struct {
self.ref_count += 1; self.ref_count += 1;
} }
pub fn release(self: *Object, env: *Self) bool {
coral.debug.assert(self.ref_count != 0);
self.ref_count -= 1;
if (self.ref_count == 0) {
coral.io.deallocate(env.allocator, self.state.userdata);
self.state.fields.deinit(env.allocator);
return false;
}
return true;
}
}; };
pub const ObjectInfo = struct { pub const ObjectInfo = struct {
@ -185,22 +170,26 @@ pub fn check(self: *Self, condition: bool, failure_message: []const u8) !void {
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
self.object_release(self.global_object);
{
var interned_iterable = InternTable.Iterable{.hashed_map = &self.interned};
while (interned_iterable.next()) |entry| {
self.object_release(entry.value);
kayomn marked this conversation as resolved Outdated

Perhaps discard could have an internal complimentary function specifically for discarding objects to save on the effort of wrapping a known type in a discriminated union only to unwrap it again back to the known type.

Perhaps `discard` could have an internal complimentary function specifically for discarding objects to save on the effort of wrapping a known type in a discriminated union only to unwrap it again back to the known type.
}
}
self.interned.deinit(self.allocator);
self.values.deinit(self.allocator); self.values.deinit(self.allocator);
self.calls.deinit(self.allocator); self.calls.deinit(self.allocator);
coral.debug.assert(self.heap.is_empty());
kayomn marked this conversation as resolved Outdated

May be worth adding an assert to check that the VM environment heap is empty by now before calling deinit.

May be worth adding an assert to check that the VM environment heap is empty by now before calling deinit.
self.heap.deinit(self.allocator);
} }
pub fn discard(self: *Self, val: types.Val) void { pub fn discard(self: *Self, val: types.Val) void {
switch (val) { switch (val) {
.object => |object| { .object => |object| self.object_release(object),
var data = self.heap.fetch(object);
if (data.release(self)) {
self.heap.remove(object);
} else {
self.heap.assign(object, data);
}
},
else => {}, else => {},
} }
} }
@ -214,6 +203,7 @@ pub fn execute_data(self: *Self, source: DataSource) types.RuntimeError!types.Va
} }
}; };
var compiled_chunk = init_compiled_chunk: {
var chunk = try Chunk.init(self, source.name); var chunk = try Chunk.init(self, source.name);
errdefer chunk.deinit(); errdefer chunk.deinit();
@ -224,7 +214,10 @@ pub fn execute_data(self: *Self, source: DataSource) types.RuntimeError!types.Va
return compile_error; return compile_error;
}; };
const script = try self.new_object(coral.io.bytes_of(&chunk), .{ break: init_compiled_chunk chunk;
};
const script = try self.new_object(coral.io.bytes_of(&compiled_chunk), .{
.identity = typeid, .identity = typeid,
.deinitializer = Behaviors.deinitialize, .deinitializer = Behaviors.deinitialize,
}); });
@ -239,25 +232,29 @@ pub fn execute_file(self: *Self, fs: file.System, file_path: file.Path) ExecuteF
defer readable_file.close(); defer readable_file.close();
var file_source = coral.list.Stack(u8){}; var file_data = coral.list.Stack(u8){};
const file_size = (try fs.query_info(file_path)).size; const file_size = (try fs.query_info(file_path)).size;
try file_source.grow(self.allocator, file_size); try file_data.grow(self.allocator, file_size);
defer file_source.deinit(self.allocator); defer file_data.deinit(self.allocator);
{ {
var file_buffer = file_source.as_buffer(self.allocator); var writable_data = coral.list.Writable{
.allocator = self.allocator,
.list = .{.stack = &file_data},
};
var stream_buffer = [_]u8{0} ** 4096; var stream_buffer = [_]u8{0} ** 4096;
if ((try coral.io.stream(file_buffer.as_writer(), readable_file.as_reader(), &stream_buffer)) != file_size) { if ((try coral.io.stream(writable_data.as_writer(), readable_file.as_reader(), &stream_buffer)) != file_size) {
return error.ReadFailure; return error.ReadFailure;
} }
} }
return try self.execute_data(.{ return try self.execute_data(.{
.name = try file_path.to_string(), .name = try file_path.to_string(),
.data = file_source.values, .data = file_data.values,
}); });
} }
@ -346,12 +343,11 @@ pub fn native_cast(self: *Self, castable: types.Ref, id: *const anyopaque, compt
try self.check(castable == .object, "invalid type conversion: object"); try self.check(castable == .object, "invalid type conversion: object");
const object = self.heap.fetch(castable.object); const object = self.heap.fetch(castable.object);
const alignment = @alignOf(Type); const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == @sizeOf(Type));
const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == alignment);
try self.check(is_expected_type, "invalid object cast: native type"); try self.check(is_expected_type, "invalid object cast: native type");
return @ptrCast(*Type, @alignCast(alignment, object.state.userdata)); return @ptrCast(*Type, @alignCast(@alignOf(Type), object.state.userdata));
} }
pub fn new_array(self: *Self) coral.io.AllocationError!types.Val { pub fn new_array(self: *Self) coral.io.AllocationError!types.Val {
@ -361,7 +357,7 @@ pub fn new_array(self: *Self) coral.io.AllocationError!types.Val {
} }
pub fn new_object(self: *Self, userdata: []const u8, info: ObjectInfo) coral.io.AllocationError!types.Val { pub fn new_object(self: *Self, userdata: []const u8, info: ObjectInfo) coral.io.AllocationError!types.Val {
const allocation = try coral.io.allocate_many(u8, userdata.len, self.allocator); const allocation = try coral.io.allocate_many(self.allocator, userdata.len, u8);
errdefer coral.io.deallocate(self.allocator, allocation); errdefer coral.io.deallocate(self.allocator, allocation);
@ -400,6 +396,28 @@ pub fn new_string(self: *Self, data: []const u8) coral.io.AllocationError!types.
}); });
} }
pub fn object_release(self: *Self, object: types.Object) void {
var data = self.heap.fetch(object);
coral.debug.assert(data.ref_count != 0);
data.ref_count -= 1;
if (data.ref_count == 0) {
data.state.info.deinitializer(.{
.env = self,
.obj = .{.object = object},
});
// TODO: Free individual key-value pairs of fields
data.state.fields.deinit(self.allocator);
coral.io.deallocate(self.allocator, data.state.userdata);
self.heap.remove(object);
} else {
self.heap.assign(object, data);
}
}
pub fn set_global(self: *Self, global_name: []const u8, value: types.Ref) coral.io.AllocationError!void { pub fn set_global(self: *Self, global_name: []const u8, value: types.Ref) coral.io.AllocationError!void {
try self.globals.assign(self.allocator, global_name, value); try self.globals.assign(self.allocator, global_name, value);
} }

View File

@ -56,6 +56,8 @@ const AppManifest = struct {
}; };
pub fn run_app(base_file_system: file.System) void { pub fn run_app(base_file_system: file.System) void {
defer heap.trace_leaks();
const Logger = struct { const Logger = struct {
const Self = @This(); const Self = @This();