feat: materialize readonly compile-time byte slices

This commit is contained in:
2026-10-30 14:23:39 +01:00
parent 6748b38eac
commit 8e790b30cc
7 changed files with 505 additions and 34 deletions
+78 -10
View File
@@ -9,12 +9,27 @@ Outcome :: struct {
trap ?diagnostics.DiagnosticId = null
allocator ?mem.Allocator = null
owned_elements ?[]mut ir.Value = null
evaluation_memory ?[]mut EvaluationMemory = null
}
#! release an escaped aggregate with its retained allocator; scalar and trap outcomes need no allocation.
#! strings and aggregate descriptors borrow the module, which must outlive this outcome.
#! releases result storage and any retained compile-time evaluation memory.
#! string IDs and aggregate descriptors borrow the module, which must outlive this outcome.
deinit proc(outcome @mut Outcome) void {
if (outcome.owned_elements) |elements| mem.free(outcome.allocator?, elements)
if (outcome.evaluation_memory) |owned| {
memory :: &owned[0]
allocator :: outcome.allocator?
for (memory.frames.items) |frame| free_frame(allocator, frame)
arraylist.deinit(&memory.frames)
arraylist.deinit(&memory.pointers)
mem.free(allocator, memory.globals)
mem.free(allocator, memory.aggregate_globals)
mem.free(allocator, memory.strings)
mem.free(allocator, memory.string_cells)
mem.free(allocator, memory.scratch)
mem.free(allocator, owned)
}
outcome.evaluation_memory = null
outcome.owned_elements = null
outcome.allocator = null
outcome.value = ir.Value{}
@@ -82,6 +97,31 @@ Frame :: struct {
addresses []mut Address
}
@hide:file
EvaluationMemory :: struct {
globals []mut GlobalState
aggregate_globals []mut ir.Value
strings []mut StringView
string_cells []mut ir.Value
scratch []mut ir.Value
pointers arraylist.ArrayList(Pointer)
frames arraylist.ArrayList(Frame)
}
#! borrows one checked byte from retained evaluation storage; null means an invalid pointer/range.
evaluation_byte proc(outcome @Outcome, module @ir.Module, pointer ir.Value, index usize) ?ir.Value {
if (outcome.evaluation_memory == null) return null
memory :: &outcome.evaluation_memory?[0]
handle :: pointer.data.unsigned
if (handle >= u64(memory.pointers.items.len)) return null
location := memory.pointers.items[usize(handle)]
if (index > maxval!(usize) - location.byte_offset) return null
location.byte_offset += index
if (pointer_address(module, location, .u8, ir.no_array)) |address|
return read_address(module, address, memory.scratch)
return null
}
#! executes typed IR with invocation/global storage released on every exit.
#! aggregate results own flat storage released by `deinit`; string IDs and shapes resolve through `module`.
run proc(
@@ -93,8 +133,7 @@ run proc(
}
#! executes an isolated compile-time module with bounded work and stack depth.
#! ownership and diagnostic identities follow run.
#! temporary execution storage uses `allocator` and is released before returning.
#! result backing, including addressed completed frames, remains owned by the outcome until `deinit`.
run_evaluation proc(
allocator mem.Allocator,
module @ir.Module,
@@ -119,8 +158,9 @@ execute proc(
return Outcome{ trap = module.entry_diagnostic }
}
transferred := false
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
defer mem.free(allocator, globals)
defer if (!transferred) mem.free(allocator, globals)
mem.set(globals, GlobalState{})
global_cells usize := 0
for (module.globals.items) |global| {
@@ -130,7 +170,7 @@ execute proc(
global_cells += cells
}
aggregate_globals :: try mem.alloc(ir.Value, allocator, global_cells)
defer mem.free(allocator, aggregate_globals)
defer if (!transferred) mem.free(allocator, aggregate_globals)
global_offset usize := 0
for (module.globals.items) |global, i| {
if (!ir.aggregate(global.type)) continue
@@ -144,7 +184,7 @@ execute proc(
if (shape.byte_size > scratch_size) scratch_size = shape.byte_size
}
scratch :: try mem.alloc(ir.Value, allocator, scratch_size)
defer mem.free(allocator, scratch)
defer if (!transferred) mem.free(allocator, scratch)
source_scratch :: try mem.alloc(ir.Value, allocator, scratch_size)
defer mem.free(allocator, source_scratch)
string_size usize := 0
@@ -153,9 +193,9 @@ execute proc(
string_size += string.len + 1
}
string_cells :: try mem.alloc(ir.Value, allocator, string_size)
defer mem.free(allocator, string_cells)
defer if (!transferred) mem.free(allocator, string_cells)
strings :: try mem.alloc(StringView, allocator, module.strings.items.len)
defer mem.free(allocator, strings)
defer if (!transferred) mem.free(allocator, strings)
string_offset usize := 0
for (module.strings.items) |string, index| {
cells :: string_cells[string_offset..string_offset + string.len + 1]
@@ -165,7 +205,12 @@ execute proc(
string_offset += cells.len
}
pointers := arraylist.init(Pointer, allocator)
defer arraylist.deinit(&pointers)
defer if (!transferred) arraylist.deinit(&pointers)
retained := arraylist.init(Frame, allocator)
defer if (!transferred) {
for (retained.items) |frame| free_frame(allocator, frame)
arraylist.deinit(&retained)
}
frames := arraylist.init(Frame, allocator)
defer {
for (frames.items) |frame| free_frame(allocator, frame)
@@ -211,6 +256,21 @@ execute proc(
globals[global_index].status = .ready
}
if (frames.items.len == 1) {
if $(bounded) {
if (pointers.items.len != 0 and (ir.aggregate(value.type) or value.type == .pointer)) {
owned :: try mem.alloc(EvaluationMemory, allocator, 1)
errdefer mem.free(allocator, owned)
try arraylist.append(&retained, completed)
_ = arraylist.pop(&frames)
owned[0] = EvaluationMemory{
globals = globals, aggregate_globals = aggregate_globals,
strings = strings, string_cells = string_cells, scratch = scratch,
pointers = pointers, frames = retained,
}
transferred = true
return Outcome{ value = value, allocator = allocator, evaluation_memory = owned }
}
}
if (ir.aggregate(value.type)) {
escaped :: try mem.alloc(ir.Value, allocator, value.data.elements.len)
memcopy!(escaped, value.data.elements)
@@ -220,6 +280,14 @@ execute proc(
return Outcome{ value = value }
}
if (completed.destination != ir.no_value) copy_value(module, &frames.items[frame_index - 1].values[usize(completed.destination)], value)
if $(bounded) {
# ponytail: retain address-bearing frames until snapshotting; trace live roots if memory grows.
if (completed.addresses.len != 0) {
try arraylist.append(&retained, completed)
_ = arraylist.pop(&frames)
continue
}
}
_ = arraylist.pop(&frames)
free_frame(allocator, completed)
continue
+61
View File
@@ -494,6 +494,67 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test {
try testing.expect_equal(usize(0), state.live)
}
evaluation_backing_survives_returns_and_releases_at_every_allocation_boundary test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)
store := diagnostics.init(mem.c_allocator)
defer diagnostics.deinit(&store)
types [1]ir.Type :: [.u8]
helper :: try add_function(&module, types[..], 0, .pointer)
try emit(&module, helper, ir.Instruction{
op = .constant, type = .u8, destination = ir.ValueId(0),
constant = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 42 } },
})
try emit(&module, helper, ir.Instruction{ op = .store_local, left = ir.ValueId(0) })
try emit(&module, helper, ir.Instruction{
op = .address_local, type = .address, pointee = .u8, destination = ir.ValueId(1),
})
try emit(&module, helper, ir.Instruction{
op = .pointer_from_address, type = .pointer, left = ir.ValueId(1), destination = ir.ValueId(2),
})
return_value(&module, helper, ir.ValueId(2))
main :: try add_function(&module, &[], 0, .pointer)
module.entry = main
try emit(&module, main, function_value(0, helper))
try emit(&module, main, ir.Instruction{
op = .call, type = .pointer, left = ir.ValueId(0), destination = ir.ValueId(1),
})
return_value(&module, main, ir.ValueId(1))
trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 })
for [false, true] |trap| {
if (trap) module.functions.items[usize(main)].blocks.items[0].terminator =
ir.Terminator{ kind = .trap, diagnostic = trap_id }
succeeded := false
for (usize(0)..usize(64)) |limit| {
state := AllocationState{ remaining = limit }
allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable }
outcome := run_evaluation(allocator, &module, &store) catch |error| {
match error {
.out_of_memory: {}
else: return .expectation_failed
}
try testing.expect_equal(usize(0), state.live)
continue
}
defer deinit(&outcome)
if (trap) {
try testing.expect_equal(trap_id, outcome.trap?)
} else {
try testing.expect(outcome.trap == null)
byte :: evaluation_byte(&outcome, &module, outcome.value, 0)
try testing.expect(byte != null)
try testing.expect_equal(u64(42), byte?.data.unsigned)
try testing.expect(evaluation_byte(&outcome, &module, outcome.value, 1) == null)
}
deinit(&outcome)
try testing.expect_equal(usize(0), state.live)
succeeded = true
break
}
try testing.expect(succeeded)
}
}
runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test {
module := ir.init(mem.c_allocator)
defer ir.deinit(&module)