feat: materialize readonly compile-time byte slices
This commit is contained in:
@@ -946,3 +946,60 @@ computed_callbacks_recompile_without_retaining_interpreter_function_ids test {
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
}
|
||||
}
|
||||
|
||||
computed_byte_slices_recompile_without_borrowing_evaluation_storage test {
|
||||
diagnostic_store := diagnostics.init(mem.c_allocator)
|
||||
defer diagnostics.deinit(&diagnostic_store)
|
||||
modules [2]mut ir.Module := undefined
|
||||
{
|
||||
session := init(mem.c_allocator)
|
||||
defer deinit(&session)
|
||||
root :: try add_package(&session)
|
||||
dependency :: try add_package(&session)
|
||||
bytes := try owned_text(
|
||||
`lib :: import "../library"
|
||||
`main proc() i16 {
|
||||
` packet :: $(lib.make())
|
||||
` if (packet.bytes.len != 4 or packet.suffix.len != 3) return -1
|
||||
` if (packet.bytes[0] != 255 or packet.bytes[1] != 0) return -2
|
||||
` if (packet.bytes[2] != 128 or packet.bytes[3] != 42) return -3
|
||||
` if (packet.suffix[0] != 65 or packet.suffix[1] != 0) return -4
|
||||
` if (packet.suffix[2] != 66 or packet.suffix.ptr[3] != 0) return -5
|
||||
` return 42
|
||||
`}
|
||||
,
|
||||
)
|
||||
defer arraylist.deinit(&bytes)
|
||||
root_source :: try add_source(&session, root, "/byte_slices/app/main.hon", &bytes, &diagnostic_store)
|
||||
imported := try owned_text(
|
||||
`Packet :: struct { bytes []u8, suffix [;0]u8 }
|
||||
`make proc() Packet {
|
||||
` bytes [4]mut u8 := undefined
|
||||
` bytes[0] = 255
|
||||
` bytes[1] = 0
|
||||
` bytes[2] = 128
|
||||
` bytes[3] = 42
|
||||
` return Packet { bytes = bytes[..], suffix = "xA\0B"[1..;0] }
|
||||
`}
|
||||
,
|
||||
)
|
||||
defer arraylist.deinit(&imported)
|
||||
_ = try add_source(&session, dependency, "/byte_slices/library/bytes.hon", &imported, &diagnostic_store)
|
||||
try arraylist.append(&session.imports, Import{
|
||||
node = declaration(&session, root_source, 0).node,
|
||||
target = dependency,
|
||||
})
|
||||
modules[0] = try compile(&session, root, &diagnostic_store)
|
||||
errdefer ir.deinit(&modules[0])
|
||||
modules[1] = try compile(&session, root, &diagnostic_store)
|
||||
}
|
||||
defer ir.deinit(&modules[0])
|
||||
defer ir.deinit(&modules[1])
|
||||
try testing.expect_equal(0, diagnostics.count(&diagnostic_store))
|
||||
for modules |module| {
|
||||
outcome := try interpreter.run(mem.c_allocator, &module, &diagnostic_store)
|
||||
defer interpreter.deinit(&outcome)
|
||||
try testing.expect(outcome.trap == null)
|
||||
try testing.expect_equal(i64(42), outcome.value.data.integer)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -2331,13 +2331,14 @@ evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Erro
|
||||
return try evaluate(state, id)
|
||||
}
|
||||
|
||||
# values leaving the temporary interpreter must own no interpreter addresses or module IDs.
|
||||
# rebuild contextual source literals and source-bound callback references before releasing it.
|
||||
# values leaving temporary evaluation must own no interpreter addresses or module IDs.
|
||||
# snapshot readonly bytes and rebuild source literals/references before releasing its backing.
|
||||
@hide:file
|
||||
serialize_value proc(
|
||||
state @mut State,
|
||||
module @ir.Module,
|
||||
checked @typecheck.Result,
|
||||
outcome @interpreter.Outcome,
|
||||
functions []ast.NodeId,
|
||||
type typecheck.Type,
|
||||
value ir.Value,
|
||||
@@ -2355,13 +2356,53 @@ serialize_value proc(
|
||||
return no_node
|
||||
}
|
||||
result := no_node
|
||||
if (type.kind == .array) {
|
||||
if (type.kind == .slice) {
|
||||
slice :: typecheck.array_type(checked, type.array_type)
|
||||
if (
|
||||
slice.mutable or slice.element.kind != .u8 or slice.element.nominal != no_node
|
||||
or (slice.sentinel != null and slice.sentinel? != 0)
|
||||
) return no_node
|
||||
pointer :: ir.aggregate_item(module, value, 0)
|
||||
length :: ir.aggregate_item(module, value, 1)
|
||||
if (pointer.uninitialized or length.uninitialized) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
|
||||
return no_node
|
||||
}
|
||||
if (
|
||||
length.data.unsigned != 0
|
||||
and interpreter.evaluation_byte(outcome, module, pointer, usize(length.data.unsigned - 1)) == null
|
||||
) return no_node
|
||||
bytes :: try mem.alloc(u8, state.allocator, usize(length.data.unsigned))
|
||||
defer mem.free(state.allocator, bytes)
|
||||
for (bytes) |_, index| {
|
||||
byte :: interpreter.evaluation_byte(outcome, module, pointer, index)
|
||||
if (byte == null) return no_node
|
||||
if (byte?.uninitialized) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
|
||||
return no_node
|
||||
}
|
||||
bytes[index] = u8(byte?.data.unsigned)
|
||||
}
|
||||
if (slice.sentinel != null) {
|
||||
sentinel :: interpreter.evaluation_byte(outcome, module, pointer, bytes.len)
|
||||
if (sentinel == null) return no_node
|
||||
if (sentinel?.uninitialized) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .read_before_initialization, span)
|
||||
return no_node
|
||||
}
|
||||
if (sentinel?.data.unsigned != 0) {
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .sentinel_mismatch, span)
|
||||
return no_node
|
||||
}
|
||||
}
|
||||
result = try ast.add_string(state.builder, span, bytes)
|
||||
} else if (type.kind == .array) {
|
||||
array :: typecheck.array_type(checked, type.array_type)
|
||||
elements := arraylist.init(ast.NodeId, state.allocator)
|
||||
defer arraylist.deinit(&elements)
|
||||
for (0..array.len) |i| {
|
||||
child :: ir.aggregate_item(module, value, i)
|
||||
node :: try serialize_value(state, module, checked, functions, array.element, child, span)
|
||||
node :: try serialize_value(state, module, checked, outcome, functions, array.element, child, span)
|
||||
if (node == no_node) return no_node
|
||||
try arraylist.append(&elements, node)
|
||||
}
|
||||
@@ -2381,7 +2422,7 @@ serialize_value proc(
|
||||
return result
|
||||
}
|
||||
child :: ir.aggregate_item(module, value, i)
|
||||
node :: try serialize_value(state, module, checked, functions, field.type, child, span)
|
||||
node :: try serialize_value(state, module, checked, outcome, functions, field.type, child, span)
|
||||
if (node == no_node) return no_node
|
||||
name := field.name
|
||||
if (name != null) name = try member_name(state, name?)
|
||||
@@ -2396,7 +2437,7 @@ serialize_value proc(
|
||||
index :: if (failed) usize(1) else usize(0)
|
||||
channel :: if (failed) container.error_type else container.element
|
||||
child :: ir.aggregate_item(module, value, index)
|
||||
result = try serialize_value(state, module, checked, functions, channel, child, span)
|
||||
result = try serialize_value(state, module, checked, outcome, functions, channel, child, span)
|
||||
if (result == no_node) return no_node
|
||||
# keep named success/error types distinct even when their members overlap.
|
||||
annotation := no_node
|
||||
@@ -2425,12 +2466,12 @@ serialize_value proc(
|
||||
value.data.function == maxval!(ir.FunctionId)
|
||||
else value.data.unsigned == maxval!(u64)
|
||||
if (absent) result = try ast.add_literal(state.builder, .null, span)
|
||||
else result = try serialize_value(state, module, checked, functions, container.element, value, span)
|
||||
else result = try serialize_value(state, module, checked, outcome, functions, container.element, value, span)
|
||||
} else if (!ir.aggregate_item(module, value, 0).data.boolean) {
|
||||
result = try ast.add_literal(state.builder, .null, span)
|
||||
} else {
|
||||
child :: ir.aggregate_item(module, value, 1)
|
||||
payload :: try serialize_value(state, module, checked, functions, container.element, child, span)
|
||||
payload :: try serialize_value(state, module, checked, outcome, functions, container.element, child, span)
|
||||
if (payload == no_node) return no_node
|
||||
name :: try ast.add_generated_identifier(state.builder, span,
|
||||
try strpool.intern(state.strings, "some"), "some")
|
||||
@@ -2635,13 +2676,16 @@ evaluate_ir_body proc(state @mut State, at, body, result_type ast.NodeId) Value
|
||||
}
|
||||
value :: outcome.value
|
||||
source_type :: typecheck.type_of(&checked, result_type)
|
||||
node :: try serialize_value(state, &module, &checked, functions.items, source_type, value, span)
|
||||
node :: try serialize_value(state, &module, &checked, &outcome, functions.items, source_type, value, span)
|
||||
if (node == no_node) {
|
||||
if (state.evaluation_diagnostic == diagnostics.no_diagnostic)
|
||||
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .comptime_value_escape, span)
|
||||
return Value{}
|
||||
}
|
||||
if (ir.aggregate(value.type) or source_type.kind == .enumeration or source_type.kind == .void)
|
||||
if (
|
||||
(ir.aggregate(value.type) and source_type.kind != .slice)
|
||||
or source_type.kind == .enumeration or source_type.kind == .void
|
||||
)
|
||||
return Value{ kind = .aggregate, node = node, annotation = result_type }
|
||||
result := try evaluate(state, node)
|
||||
result.annotation = result_type
|
||||
|
||||
Reference in New Issue
Block a user