feat: materialize nonzero-sentinel compile-time byte slices

Own nonzero trailing sentinels in canonical string-backed snapshots and preserve specialization identity across computed and direct views.

Verified: bro build; bro test (296 passed); python3 tests/native.py (1026 interpreter/native comparisons at -O0/-O2, 5 native allocator programs, LLVM output and CLI errors).
This commit is contained in:
2026-10-30 15:26:05 +01:00
parent 8e790b30cc
commit c8890b4355
5 changed files with 165 additions and 21 deletions
+5 -2
View File
@@ -965,6 +965,8 @@ computed_byte_slices_recompile_without_borrowing_evaluation_storage test {
` 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
` if (packet.marked.len != 2 or packet.marked.ptr[2] != 255) return -6
` if (packet.marked[0] != 40 or packet.marked[1] != 2) return -7
` return 42
`}
,
@@ -972,14 +974,15 @@ computed_byte_slices_recompile_without_borrowing_evaluation_storage test {
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 }
`Packet :: struct { bytes []u8, suffix [;0]u8, marked [;255]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] }
` marked [3]mut u8 := [40, 2, 255]
` return Packet { bytes = bytes[..], suffix = "xA\0B"[1..;0], marked = marked[..2;255] }
`}
,
)
+33 -9
View File
@@ -2325,6 +2325,7 @@ evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Erro
and !try type_array_initialized(state, id, id)) return Value{}
if (type != no_node and (contextual_literal(state, id)
or ast.expr_kind(&state.tree, id) == .block_expr
or ast.expr_kind(&state.tree, id) == .slice
or ast.expr_kind(&state.tree, type) == .fallible_type))
return try evaluate_ir(state, id, type)
}
@@ -2332,7 +2333,7 @@ evaluate_expected proc(state @mut State, id, annotation ast.NodeId) Value ! Erro
}
# values leaving temporary evaluation must own no interpreter addresses or module IDs.
# snapshot readonly bytes and rebuild source literals/references before releasing its backing.
# snapshot readonly bytes and rebuild source literals/references before releasing their backing.
@hide:file
serialize_value proc(
state @mut State,
@@ -2358,10 +2359,7 @@ serialize_value proc(
result := no_node
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
if (slice.mutable or slice.element.kind != .u8 or slice.element.nominal != no_node) return no_node
pointer :: ir.aggregate_item(module, value, 0)
length :: ir.aggregate_item(module, value, 1)
if (pointer.uninitialized or length.uninitialized) {
@@ -2372,9 +2370,13 @@ serialize_value proc(
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))
nonzero_sentinel :: slice.sentinel != null and slice.sentinel? != 0
length_bytes :: usize(length.data.unsigned)
if (nonzero_sentinel and length_bytes == maxval!(usize)) return no_node
extra :: if (nonzero_sentinel) usize(1) else usize(0)
bytes :: try mem.alloc(u8, state.allocator, length_bytes + extra)
defer mem.free(state.allocator, bytes)
for (bytes) |_, index| {
for (0..length_bytes) |index| {
byte :: interpreter.evaluation_byte(outcome, module, pointer, index)
if (byte == null) return no_node
if (byte?.uninitialized) {
@@ -2384,18 +2386,24 @@ serialize_value proc(
bytes[index] = u8(byte?.data.unsigned)
}
if (slice.sentinel != null) {
sentinel :: interpreter.evaluation_byte(outcome, module, pointer, bytes.len)
sentinel :: interpreter.evaluation_byte(outcome, module, pointer, length_bytes)
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) {
if (sentinel?.data.unsigned != u64(slice.sentinel?)) {
state.evaluation_diagnostic = try diagnostics.add(state.diagnostics, .sentinel_mismatch, span)
return no_node
}
if (nonzero_sentinel) bytes[length_bytes] = u8(slice.sentinel?)
}
result = try ast.add_string(state.builder, span, bytes)
if (nonzero_sentinel) {
end :: try ast.add_integer(state.builder, span, u64(length_bytes))
sentinel :: try ast.add_integer(state.builder, span, u64(slice.sentinel?))
result = try ast.add_slice(state.builder, span, result, null, end, sentinel)
}
} else if (type.kind == .array) {
array :: typecheck.array_type(checked, type.array_type)
elements := arraylist.init(ast.NodeId, state.allocator)
@@ -2510,6 +2518,16 @@ constant_equal proc(state @mut State, left, right ast.NodeId) bool ! Error {
b :: ast.typed_constant(&state.tree, right)
return try same_type_context(state, a.type, b.type, true) and try constant_equal(state, a.value, b.value)
}
if (kind == .slice) {
a :: ast.slice_expr(&state.tree, left)
b :: ast.slice_expr(&state.tree, right)
# serialized nonzero-sentinel views have a canonical owned string and explicit end.
return (
try constant_equal(state, a.base, b.base)
and try constant_equal(state, a.end?, b.end?)
and try constant_equal(state, a.sentinel?, b.sentinel?)
)
}
if (kind == .array_literal) {
count :: ast.array_element_count(&state.tree, left)
if (count != ast.array_element_count(&state.tree, right)) return false
@@ -2547,6 +2565,11 @@ constant_hash proc(state @mut State, id ast.NodeId) usize ! Error {
value :: ast.typed_constant(&state.tree, id)
return mix(mix(hash, try type_hash_context(state, value.type, true)), try constant_hash(state, value.value))
}
if (kind == .slice) {
view :: ast.slice_expr(&state.tree, id)
return mix(mix(mix(hash, try constant_hash(state, view.base)),
try constant_hash(state, view.end?)), try constant_hash(state, view.sentinel?))
}
if (kind == .array_literal) {
result := hash
for (0..ast.array_element_count(&state.tree, id)) |i|
@@ -2684,6 +2707,7 @@ evaluate_ir_body proc(state @mut State, at, body, result_type ast.NodeId) Value
}
if (
(ir.aggregate(value.type) and source_type.kind != .slice)
or (source_type.kind == .slice and ast.expr_kind(&state.tree, node) == .slice)
or source_type.kind == .enumeration or source_type.kind == .void
)
return Value{ kind = .aggregate, node = node, annotation = result_type }