feat: complete memory representation gate

This commit is contained in:
2026-10-26 11:49:25 +01:00
parent 7856f4f963
commit cd64afbb40
16 changed files with 2103 additions and 159 deletions
+252 -25
View File
@@ -42,6 +42,9 @@ Address :: struct {
scalar ?@mut ir.Value = null
initialized ?@mut bool = null
raw bool = false
byte_view bool = false
readonly bool = false
bytes []mut ir.Value = &[]
# source handles retain root identity and native offsets, never frame-local address IDs.
root ?@mut ir.Value = null
root_type ir.Type = .invalid
@@ -55,6 +58,14 @@ Pointer :: struct {
root_type ir.Type
root_array ir.ArrayId
byte_offset usize
bytes []mut ir.Value = &[]
readonly bool = false
}
@hide:file
StringView :: struct {
root ir.Value = ir.Value{ type = .u8 }
cells []mut ir.Value
}
@hide:file
@@ -103,6 +114,32 @@ run proc(
globals[i].value = aggregate_view(global.type, global.array_type, aggregate_globals[global_offset..global_offset + cells])
global_offset += cells
}
# byte reinterpretation reuses run-wide scratch; loads are copied before the next instruction.
scratch_size usize := 16
for (module.arrays.items) |shape| {
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)
source_scratch :: try mem.alloc(ir.Value, allocator, scratch_size)
defer mem.free(allocator, source_scratch)
string_size usize := 0
for (module.strings.items) |string| {
if (string.len == maxval!(usize) or string.len + 1 > maxval!(usize) - string_size) return .storage_limit_reached
string_size += string.len + 1
}
string_cells :: try mem.alloc(ir.Value, allocator, string_size)
defer mem.free(allocator, string_cells)
strings :: try mem.alloc(StringView, allocator, module.strings.items.len)
defer mem.free(allocator, strings)
string_offset usize := 0
for (module.strings.items) |string, index| {
cells :: string_cells[string_offset..string_offset + string.len + 1]
for (string.allocation[..string.len]) |byte, i| cells[i] = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = u64(byte) } }
cells[string.len] = ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } }
strings[index] = StringView{ cells = cells }
string_offset += cells.len
}
pointers := arraylist.init(Pointer, allocator)
defer arraylist.deinit(&pointers)
frames := arraylist.init(Frame, allocator)
@@ -110,6 +147,9 @@ run proc(
for (frames.items) |frame| free_frame(allocator, frame)
arraylist.deinit(&frames)
}
if (module.functions.items[usize(entry)].external_name.len != 0) {
return try runtime_trap(diagnostic_store, .unsupported_foreign_call, module.functions.items[usize(entry)].span)
}
try push_frame(allocator, module, &frames, entry, ir.no_value, null)
while frames.items.len != 0 {
@@ -210,7 +250,14 @@ run proc(
}
}
.call: {
callee :: frames.items[frame_index].values[usize(instruction.left)].data.function
callable :: frames.items[frame_index].values[usize(instruction.left)]
if (callable.data.function == maxval!(ir.FunctionId)) {
return try runtime_trap(diagnostic_store, .null_unwrap, instruction.span)
}
callee :: callable.data.function
if (module.functions.items[usize(callee)].external_name.len != 0) {
return try runtime_trap(diagnostic_store, .unsupported_foreign_call, instruction.span)
}
try push_frame(allocator, module, &frames, callee, instruction.destination, null)
# frame indices survive stack growth; pointers into frames.items do not.
for (0..instruction.arguments_count) |argument_index| {
@@ -251,6 +298,95 @@ run proc(
continue
}
.union_tag: value = ir.union_tag(module, frames.items[frame_index].values[usize(instruction.left)])
.string_length: {
string :: frames.items[frame_index].values[usize(instruction.left)].data.string
value = ir.Value{ type = .usize, data = ir.ValueData{ unsigned = u64(ir.string_bytes(module, string).len) } }
}
.string_pointer: {
string :: frames.items[frame_index].values[usize(instruction.left)].data.string
view :: &strings[usize(string)]
value = try pointer_value(&pointers, Pointer{
root = &view.root, root_type = .u8, root_array = ir.no_array,
byte_offset = 0, bytes = view.cells, readonly = true,
})
}
.slice_check: {
start :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned
end :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
if (start > end) return try runtime_trap(diagnostic_store, .invalid_slice_bounds, instruction.span)
if (instruction.extra != ir.no_value and end > frames.items[frame_index].values[usize(instruction.extra)].data.unsigned) {
return try runtime_trap(diagnostic_store, .invalid_slice_bounds, instruction.span)
}
continue
}
.optional_check: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
absent := operand.data.unsigned == maxval!(u64)
if (operand.type == .bool) absent = !operand.data.boolean
else if (operand.type == .function) absent = operand.data.function == maxval!(ir.FunctionId)
if (absent) return try runtime_trap(diagnostic_store, .null_unwrap, instruction.span)
continue
}
.pointer_cast: {
value = frames.items[frame_index].values[usize(instruction.left)]
if (value.type == .function) {
bits :: if (value.data.function == maxval!(ir.FunctionId)) maxval!(u64) else u64(value.data.function)
value.data = ir.ValueData{ unsigned = bits }
}
value.type = instruction.type
}
.sentinel_check: {
handle :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned
if (handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
address :: pointer_address(module, pointers.items[usize(handle)], instruction.pointee, instruction.array_type)
if (address == null) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
actual :: read_address(module, address?, scratch)
expected :: frames.items[frame_index].values[usize(instruction.right)]
if (!equal_value(module, actual, expected)) return try runtime_trap(diagnostic_store, .sentinel_mismatch, instruction.span)
continue
}
.memory_copy, .memory_set: {
count :: frames.items[frame_index].values[usize(instruction.extra)].data.unsigned
width :: ir.storage_size(module, instruction.pointee, instruction.array_type)
if (count == 0 or width == 0) continue
if (count > u64(divtrunc!(maxval!(usize), width))) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
size :: usize(count) * width
destination_handle :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned
if (destination_handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
destination := pointers.items[usize(destination_handle)]
if (destination.readonly or !pointer_range(module, destination, size)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
source := destination
if (instruction.op == .memory_copy) {
source_handle :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
if (source_handle >= u64(pointers.items.len)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
source = pointers.items[usize(source_handle)]
if (!pointer_range(module, source, size)) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
if (ptrcast!(@usize, &source.root)^ == ptrcast!(@usize, &destination.root)^) {
distance :: if (source.byte_offset > destination.byte_offset) source.byte_offset - destination.byte_offset else destination.byte_offset - source.byte_offset
if (distance < size) return try runtime_trap(diagnostic_store, .overlapping_memory_copy, instruction.span)
}
}
destination_size :: pointer_size(module, destination)
destination_bytes :: scratch[..destination_size]
mem.set(destination_bytes, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
ir.write_storage(module, destination_bytes, destination.root^)
target :: destination_bytes[destination.byte_offset..destination.byte_offset + size]
if (instruction.op == .memory_copy) {
source_bytes := source.bytes
if (source_bytes.len == 0) {
source_bytes = source_scratch[..pointer_size(module, source)]
mem.set(source_bytes, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
ir.write_storage(module, source_bytes, source.root^)
}
memcopy!(target, source_bytes[source.byte_offset..source.byte_offset + size])
} else {
element :: frames.items[frame_index].values[usize(instruction.right)]
for (0..usize(count)) |index| ir.write_storage(module, target[index * width..], element)
}
restored :: ir.read_storage(module, destination.root_type, destination.root_array, destination_bytes, true)
copy_value(module, destination.root, restored)
continue
}
.index: {
base :: frames.items[frame_index].values[usize(instruction.left)]
index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
@@ -324,15 +460,15 @@ run proc(
if (index >= u64(shape.len)) {
return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span)
}
width :: if (base.raw) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape)
width :: if (base.raw or base.byte_view) ir.storage_size(module, shape.element, shape.element_array) else element_cells(module, shape)
start :: usize(index) * width
address := base
address.type = instruction.pointee
address.array_type = instruction.array_type
address.cells = base.cells[start..start + width]
if (!base.byte_view) address.cells = base.cells[start..start + width]
address.scalar = null
address.byte_offset += usize(index) * ir.storage_size(module, shape.element, shape.element_array)
if (!address.raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
if (!address.raw and !address.byte_view and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } }
}
@@ -345,21 +481,21 @@ run proc(
if base.initialized |initialized| {
if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
if (!ir.union_active(module, aggregate_view(.record, base.array_type, base.cells), usize(instruction.slot))) {
if (!ir.union_active(module, read_address(module, base, scratch), usize(instruction.slot))) {
return try runtime_trap(diagnostic_store, .inactive_union_field, instruction.span)
}
}
raw :: base.raw or shape.is_union
width :: if (raw) ir.storage_size(module, field.type, field.array_type) else if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
width :: if (raw or base.byte_view) ir.storage_size(module, field.type, field.array_type) else if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
start :: if (raw) field.byte_offset else field.offset
address := base
address.type = instruction.pointee
address.array_type = instruction.array_type
address.cells = base.cells[start..start + width]
if (!base.byte_view) address.cells = base.cells[start..start + width]
address.scalar = null
address.raw = raw
address.byte_offset += field.byte_offset
if (!raw and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
if (!raw and !address.byte_view and !ir.aggregate(address.type)) address.scalar = &address.cells[0]
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } }
}
@@ -374,6 +510,8 @@ run proc(
root_type = address.root_type,
root_array = address.root_array,
byte_offset = address.byte_offset,
readonly = address.readonly,
bytes = address.bytes,
})
}
.pointer_address: {
@@ -382,17 +520,7 @@ run proc(
return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
}
location :: pointers.items[usize(handle)]
root := Address{
type = location.root_type,
array_type = location.root_array,
cells = &[],
root = location.root,
root_type = location.root_type,
root_array = location.root_array,
}
if (ir.aggregate(root.type)) root.cells = root.root?^.data.elements
else root.scalar = root.root
if (project_pointer(module, root, location.byte_offset, instruction.pointee, instruction.array_type)) |address| {
if (pointer_address(module, location, instruction.pointee, instruction.array_type)) |address| {
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{ type = .address, data = ir.ValueData{ address = u32(instruction.destination) } }
} else {
@@ -411,7 +539,7 @@ run proc(
return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
}
address.byte_offset += usize(index) * stride
if (address.byte_offset > ir.storage_size(module, address.root_type, address.root_array)) {
if (address.byte_offset > pointer_size(module, address)) {
return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
}
value = try pointer_value(&pointers, address)
@@ -426,13 +554,11 @@ run proc(
}
}
if (instruction.op == .load_address) {
if (address.raw) value = ir.read_storage(module, address.type, address.array_type, address.cells, true)
else value = if (ir.aggregate(address.type)) aggregate_view(address.type, address.array_type, address.cells) else address.scalar?^
value = read_address(module, address, scratch)
} else {
source :: frames.items[frame_index].values[usize(instruction.right)]
if (address.raw) ir.write_storage(module, address.cells, source)
else if (ir.aggregate(address.type)) ir.copy_aggregate(module, aggregate_view(address.type, address.array_type, address.cells), source)
else address.scalar?^ = source
if (address.readonly) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
write_address(module, address, source, scratch)
continue
}
}
@@ -526,6 +652,11 @@ run proc(
.not_equal: left.data.boolean != right.data.boolean
else: unreachable
}
.function: result = match instruction.op {
.equal: left.data.function == right.data.function
.not_equal: left.data.function != right.data.function
else: unreachable
}
.pointer: result = match instruction.op {
.equal: left.data.unsigned == right.data.unsigned
.not_equal: left.data.unsigned != right.data.unsigned
@@ -543,6 +674,102 @@ run proc(
unreachable
}
@hide:file
pointer_size proc(module @ir.Module, pointer Pointer) usize {
if (pointer.bytes.len != 0) return pointer.bytes.len
return ir.storage_size(module, pointer.root_type, pointer.root_array)
}
@hide:file
pointer_range proc(module @ir.Module, pointer Pointer, size usize) bool {
bound :: pointer_size(module, pointer)
return pointer.byte_offset <= bound and size <= bound - pointer.byte_offset
}
@hide:file
pointer_address proc(module @ir.Module, pointer Pointer, type ir.Type, shape ir.ArrayId) ?Address {
size :: ir.storage_size(module, type, shape)
if (!pointer_range(module, pointer, size)) return null
base := Address{
type = pointer.root_type, array_type = pointer.root_array, cells = &[],
root = pointer.root, root_type = pointer.root_type, root_array = pointer.root_array,
readonly = pointer.readonly, bytes = pointer.bytes,
}
if (pointer.bytes.len != 0) {
base.type = type
base.array_type = shape
base.cells = pointer.bytes[pointer.byte_offset..pointer.byte_offset + size]
base.raw = true
base.byte_offset = pointer.byte_offset
return base
}
if (ir.aggregate(base.type)) base.cells = pointer.root^.data.elements
else base.scalar = pointer.root
if (project_pointer(module, base, pointer.byte_offset, type, shape)) |address| return address
# differently typed projections retain the original root and native byte offset.
base.type = type
base.array_type = shape
base.byte_offset = pointer.byte_offset
base.byte_view = true
base.scalar = null
return base
}
@hide:file
read_address proc(module @ir.Module, address Address, scratch []mut ir.Value) ir.Value {
if (address.byte_view) {
size :: ir.storage_size(module, address.root_type, address.root_array)
cells :: scratch[..size]
mem.set(cells, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
ir.write_storage(module, cells, address.root?^)
return ir.read_storage(module, address.type, address.array_type, cells[address.byte_offset..], true)
}
if (address.raw) return ir.read_storage(module, address.type, address.array_type, address.cells, true)
if (ir.aggregate(address.type)) return aggregate_view(address.type, address.array_type, address.cells)
return address.scalar?^
}
@hide:file
write_address proc(module @ir.Module, address Address, source ir.Value, scratch []mut ir.Value) void {
if (address.byte_view) {
size :: ir.storage_size(module, address.root_type, address.root_array)
cells :: scratch[..size]
mem.set(cells, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
ir.write_storage(module, cells, address.root?^)
ir.write_storage(module, cells[address.byte_offset..], source)
restored :: ir.read_storage(module, address.root_type, address.root_array, cells, true)
copy_value(module, address.root?, restored)
} else if (address.raw) ir.write_storage(module, address.cells, source)
else if (ir.aggregate(address.type)) ir.copy_aggregate(module, aggregate_view(address.type, address.array_type, address.cells), source)
else address.scalar?^ = source
}
@hide:file
equal_value proc(module @ir.Module, left, right ir.Value) bool {
if (ir.aggregate(left.type)) {
shape :: module.arrays.items[usize(left.array_type)]
if (shape.is_union) {
for (left.data.elements) |byte, index| {
if (byte.data.unsigned != right.data.elements[index].data.unsigned) return false
}
return true
}
count :: if (shape.kind == .record) shape.fields.len else shape.len
for (0..count) |index| {
if (!equal_value(module, ir.aggregate_item(module, left, index), ir.aggregate_item(module, right, index))) return false
}
return true
}
return match left.type {
.float: left.data.real == right.data.real
.bool: left.data.boolean == right.data.boolean
.function: left.data.function == right.data.function
.string: left.data.string == right.data.string
.void: true
else: left.data.unsigned == right.data.unsigned
}
}
@hide:file
pointer_value proc(pointers @mut arraylist.ArrayList(Pointer), address Pointer) ir.Value ! mem.AllocError {
# ponytail: linear location interning; use a location-keyed map if distinct-address workloads grow.