Add Leak Detection to Ona Heap Allocator #15
| @ -49,7 +49,7 @@ pub const Stacking = struct { | ||||
| 	} | ||||
| 
 | ||||
| 	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); | ||||
| 
 | ||||
|  | ||||
| @ -7,6 +7,7 @@ pub const AllocationError = error { | ||||
| }; | ||||
| 
 | ||||
| pub const AllocationOptions = struct { | ||||
| 	return_address: usize, | ||||
| 	allocation: ?[]u8 = null, | ||||
| 	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 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) { | ||||
| 		@compileError("Cannot allocate memory for 0-byte type " ++ @typeName(Type)); | ||||
| 	} | ||||
| 
 | ||||
| 	return @ptrCast([*]Type, @alignCast(@alignOf(Type), allocator.invoke(.{.size = @sizeOf(Type) * amount}) orelse { | ||||
| 		return error.OutOfMemory; | ||||
| 	}))[0 .. amount]; | ||||
| 	return @ptrCast([*]Type, @alignCast(@alignOf(Type), allocator.invoke(.{ | ||||
| 		.size = @sizeOf(Type) * amount, | ||||
| 		.return_address = @returnAddress(), | ||||
| 	}) orelse return error.OutOfMemory))[0 .. amount]; | ||||
| } | ||||
| 
 | ||||
| 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)); | ||||
| 	} | ||||
| 
 | ||||
| 	const allocation = @ptrCast(*Type, @alignCast(@alignOf(Type), allocator.invoke(.{.size = @sizeOf(Type)}) orelse { | ||||
| 		return error.OutOfMemory; | ||||
| 	})); | ||||
| 	const allocation = @ptrCast(*Type, @alignCast(@alignOf(Type), allocator.invoke(.{ | ||||
| 		.size = @sizeOf(Type), | ||||
| 		.return_address = @returnAddress(), | ||||
| 	}) orelse return error.OutOfMemory)); | ||||
| 
 | ||||
| 	allocation.* = value; | ||||
| 
 | ||||
| @ -232,17 +194,16 @@ pub fn copy(target: []u8, source: []const u8) void { | ||||
| } | ||||
| 
 | ||||
| pub fn deallocate(allocator: Allocator, allocation: anytype) void { | ||||
| 	const Allocation = @TypeOf(allocation); | ||||
| 
 | ||||
| 	switch (@typeInfo(Allocation)) { | ||||
| 		.Pointer => |allocation_pointer| { | ||||
| 	switch (@typeInfo(@TypeOf(allocation))) { | ||||
| 		.Pointer => |pointer| { | ||||
| 			_ = allocator.invoke(.{ | ||||
| 				.allocation = switch (allocation_pointer.size) { | ||||
| 					.One => @ptrCast([*]u8, allocation)[0 .. @sizeOf(Allocation)], | ||||
| 					.Slice => @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(Allocation) * allocation.len)], | ||||
| 				.allocation = switch (pointer.size) { | ||||
| 					.One => @ptrCast([*]u8, allocation)[0 .. @sizeOf(pointer.child)], | ||||
| 					.Slice => @ptrCast([*]u8, allocation.ptr)[0 .. (@sizeOf(pointer.child) * allocation.len)], | ||||
| 					.Many, .C => @compileError("length of allocation must be known to deallocate"), | ||||
| 				}, | ||||
| 
 | ||||
| 				.return_address = @returnAddress(), | ||||
| 				.size = 0, | ||||
| 			}); | ||||
| 		}, | ||||
|  | ||||
| @ -17,19 +17,6 @@ pub fn Stack(comptime Value: type) type { | ||||
| 		/// | ||||
| 		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. | ||||
| 		/// | ||||
| @ -52,7 +39,7 @@ pub fn Stack(comptime Value: type) type { | ||||
| 				return; | ||||
| 			} | ||||
| 
 | ||||
| 			io.deallocate(allocator, self.values); | ||||
| 			io.deallocate(allocator, self.values.ptr[0 .. self.capacity]); | ||||
| 
 | ||||
| 			self.values = &.{}; | ||||
| 			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 { | ||||
| 			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); | ||||
| 
 | ||||
| @ -96,7 +83,7 @@ pub fn Stack(comptime Value: type) type { | ||||
| 					values[index] = self.values[index]; | ||||
| 				} | ||||
| 
 | ||||
| 				io.deallocate(allocator, self.values); | ||||
| 				io.deallocate(allocator, self.values.ptr[0 .. self.capacity]); | ||||
| 			} | ||||
| 
 | ||||
| 			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 { | ||||
| 			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); | ||||
| 			} | ||||
| 
 | ||||
| @ -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), | ||||
| 		}; | ||||
| 	} | ||||
| }; | ||||
|  | ||||
| @ -52,17 +52,6 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type | ||||
| 			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`. | ||||
| 		/// | ||||
| @ -81,6 +70,17 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type | ||||
| 			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`. | ||||
| 		/// | ||||
| @ -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 { | ||||
| 			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); | ||||
| 
 | ||||
| @ -147,12 +147,20 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type | ||||
| 
 | ||||
| 			debug.assert(entry.* == .free_index); | ||||
| 
 | ||||
| 			self.count += 1; | ||||
| 			self.free_index = entry.free_index; | ||||
| 			entry.* = .{.value = value}; | ||||
| 
 | ||||
| 			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`. | ||||
| 		/// | ||||
| @ -161,6 +169,7 @@ pub fn Map(comptime index_int: std.builtin.Type.Int, comptime Value: type) type | ||||
| 
 | ||||
| 			debug.assert(entry.* == .value); | ||||
| 
 | ||||
| 			self.count -= 1; | ||||
| 			entry.* = .{.free_index = self.free_index}; | ||||
| 			self.free_index = index; | ||||
| 		} | ||||
|  | ||||
| @ -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. | ||||
| 		/// | ||||
| @ -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 { | ||||
| 			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 { | ||||
| 				io.deallocate(allocator, self.table); | ||||
|  | ||||
| @ -1,54 +1,254 @@ | ||||
| const builtin = @import("builtin"); | ||||
| 
 | ||||
| const coral = @import("coral"); | ||||
| 
 | ||||
| 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 { | ||||
| 	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 { | ||||
| 		if (options.size == 0) { | ||||
| 			if (options.allocation) |allocation| { | ||||
| 				if (allocation.ptr != &empty_allocation) { | ||||
| 					ext.SDL_free(allocation.ptr); | ||||
| 				allocation_info.trace.addAddr(return_address, ""); | ||||
| 
 | ||||
| 				self.allocation_info_head = allocation_info; | ||||
| 
 | ||||
| 				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"); | ||||
| 				} | ||||
| 
 | ||||
| 				self.live_allocations -= 1; | ||||
| 				if (self.allocation_info_head) |allocation_info_head| { | ||||
| 					if (target_allocation_info == allocation_info_head) { | ||||
| 						self.allocation_info_head = allocation_info_head.next_info; | ||||
| 
 | ||||
| 				return null; | ||||
| 			} | ||||
| 						ext.SDL_free(target_allocation_info); | ||||
| 
 | ||||
| 			self.live_allocations += 1; | ||||
| 						return; | ||||
| 					} | ||||
| 
 | ||||
| 			return &empty_allocation; | ||||
| 					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); | ||||
| 			}, | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 		if (options.allocation) |allocation| { | ||||
| 			if (ext.SDL_realloc(allocation.ptr, options.size)) |reallocation| { | ||||
| 				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, reallocation)[0 .. options.size]; | ||||
| 			} | ||||
| 				if (target_allocation_info.size != allocation.len) { | ||||
| 					@panic("incorrect allocation length for reallocating"); | ||||
| 				} | ||||
| 
 | ||||
| 				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]; | ||||
| 			}, | ||||
| 		} | ||||
| 
 | ||||
| 		if (ext.SDL_malloc(options.size)) |allocation| { | ||||
| 			self.live_allocations += 1; | ||||
| 
 | ||||
| 			return @ptrCast([*]u8, allocation)[0 .. options.size]; | ||||
| 		} | ||||
| 
 | ||||
| 		return null; | ||||
| 	} | ||||
| }; | ||||
| 
 | ||||
| var context = Context{ | ||||
| 	.live_allocations = 0, | ||||
| }; | ||||
| /// | ||||
| /// Heap context. | ||||
| /// | ||||
| var context = Context{}; | ||||
| 
 | ||||
| /// | ||||
| /// 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 => {}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -53,7 +53,7 @@ fn clear_error_details(self: *Self) void { | ||||
| pub fn compile(self: *Self, data: []const u8) types.RuntimeError!void { | ||||
| 	var ast = try Ast.init(self.env.allocator); | ||||
| 
 | ||||
| 	errdefer ast.deinit(); | ||||
| 	defer ast.deinit(); | ||||
| 
 | ||||
| 	{ | ||||
| 		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) { | ||||
| 				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, | ||||
| 					.name = ast.error_message, | ||||
| 				}) catch return error.OutOfMemory; | ||||
|  | ||||
| @ -69,21 +69,6 @@ const Object = struct { | ||||
| 
 | ||||
| 		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 { | ||||
| @ -185,22 +170,26 @@ pub fn check(self: *Self, condition: bool, failure_message: []const u8) !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); | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	self.interned.deinit(self.allocator); | ||||
| 	self.values.deinit(self.allocator); | ||||
| 	self.calls.deinit(self.allocator); | ||||
| 	coral.debug.assert(self.heap.is_empty()); | ||||
| 	self.heap.deinit(self.allocator); | ||||
| } | ||||
| 
 | ||||
| pub fn discard(self: *Self, val: types.Val) void { | ||||
| 	switch (val) { | ||||
| 		.object => |object| { | ||||
| 			var data = self.heap.fetch(object); | ||||
| 
 | ||||
| 			if (data.release(self)) { | ||||
| 				self.heap.remove(object); | ||||
| 			} else { | ||||
| 				self.heap.assign(object, data); | ||||
| 			} | ||||
| 		}, | ||||
| 
 | ||||
| 		.object => |object| self.object_release(object), | ||||
| 		else => {}, | ||||
| 	} | ||||
| } | ||||
| @ -214,17 +203,21 @@ pub fn execute_data(self: *Self, source: DataSource) types.RuntimeError!types.Va | ||||
| 		} | ||||
| 	}; | ||||
| 
 | ||||
| 	var chunk = try Chunk.init(self, source.name); | ||||
| 	var compiled_chunk = init_compiled_chunk: { | ||||
| 		var chunk = try Chunk.init(self, source.name); | ||||
| 
 | ||||
| 	errdefer chunk.deinit(); | ||||
| 		errdefer chunk.deinit(); | ||||
| 
 | ||||
| 	chunk.compile(source.data) catch |compile_error| { | ||||
| 		self.reporter.invoke(chunk.error_details()); | ||||
| 		chunk.compile(source.data) catch |compile_error| { | ||||
| 			self.reporter.invoke(chunk.error_details()); | ||||
| 
 | ||||
| 		return compile_error; | ||||
| 			return compile_error; | ||||
| 		}; | ||||
| 
 | ||||
| 		break: init_compiled_chunk chunk; | ||||
| 	}; | ||||
| 
 | ||||
| 	const script = try self.new_object(coral.io.bytes_of(&chunk), .{ | ||||
| 	const script = try self.new_object(coral.io.bytes_of(&compiled_chunk), .{ | ||||
| 		.identity = typeid, | ||||
| 		.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(); | ||||
| 
 | ||||
| 	var file_source = coral.list.Stack(u8){}; | ||||
| 	var file_data = coral.list.Stack(u8){}; | ||||
| 	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; | ||||
| 
 | ||||
| 		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 try self.execute_data(.{ | ||||
| 		.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"); | ||||
| 
 | ||||
| 	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 == alignment); | ||||
| 	const is_expected_type = (object.state.info.identity == id) and (object.state.userdata.len == @sizeOf(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 { | ||||
| @ -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 { | ||||
| 	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); | ||||
| 
 | ||||
| @ -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 { | ||||
| 	try self.globals.assign(self.allocator, global_name, value); | ||||
| } | ||||
|  | ||||
| @ -56,6 +56,8 @@ const AppManifest = struct { | ||||
| }; | ||||
| 
 | ||||
| pub fn run_app(base_file_system: file.System) void { | ||||
| 	defer heap.trace_leaks(); | ||||
| 
 | ||||
| 	const Logger = struct { | ||||
| 		const Self = @This(); | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user