Files

1172 lines
58 KiB
Plaintext

import "@std/mem"
import "@std/arraylist"
import "@std/enummap"
import "@source/ir"
import "@source/diagnostics"
Outcome :: struct {
value ir.Value = ir.Value{}
trap ?diagnostics.DiagnosticId = null
allocator ?mem.Allocator = null
owned_elements ?[]mut ir.Value = null
evaluation_memory ?[]mut EvaluationMemory = null
}
#! 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{}
}
@hide:file
Slot :: struct {
initialized bool = false
value ir.Value = ir.Value{}
}
@hide:file
GlobalStatus :: enum { uninitialized, initializing, ready }
@hide:file
GlobalState :: struct {
status GlobalStatus = .uninitialized
value ir.Value = ir.Value{}
}
@hide:file
Address :: struct {
type ir.Type
array_type ir.ArrayId = ir.no_array
cells []mut ir.Value
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
root_array ir.ArrayId = ir.no_array
byte_offset usize = 0
}
@hide:file
Pointer :: struct {
root @mut ir.Value
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
Frame :: struct {
function ir.FunctionId
block ir.BlockId
next_instruction usize = 0
destination ir.ValueId = ir.no_value
global ?usize = null
locals []mut Slot
values []mut ir.Value
aggregate []mut ir.Value
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 typed element from retained evaluation storage; null means an invalid pointer/range.
#! aggregate storage may borrow scratch valid only until the next evaluation accessor call.
evaluation_element proc(
outcome @Outcome,
module @ir.Module,
pointer ir.Value,
index usize,
type ir.Type,
shape ir.ArrayId,
) ?ir.Value {
if (outcome.evaluation_memory == null or pointer.type != .pointer) 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)]
stride :: ir.storage_size(module, type, shape)
if (stride != 0 and index > divtrunc!(maxval!(usize) - location.byte_offset, stride)) return null
location.byte_offset += index * stride
if (pointer_address(module, location, type, shape)) |address|
return read_address(module, address, memory.scratch)
return null
}
#! 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 {
return evaluation_element(outcome, module, pointer, index, .u8, ir.no_array)
}
#! 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(
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
return try execute(false, allocator, module, diagnostic_store, null)
}
#! executes an isolated compile-time module with bounded work and stack depth.
#! result backing, including addressed completed frames, remains owned by the outcome until `deinit`.
run_evaluation proc(
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
remaining := usize(100000)
return try execute(true, allocator, module, diagnostic_store, &remaining)
}
@hide:file
execute proc(
$bounded bool,
allocator mem.Allocator,
module @ir.Module,
diagnostic_store @mut diagnostics.Store,
remaining ?@mut usize,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
entry ir.FunctionId := ir.FunctionId(0)
if module.entry |function| {
entry = function
} else {
return Outcome{ trap = module.entry_diagnostic }
}
transferred := false
globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len)
defer if (!transferred) mem.free(allocator, globals)
mem.set(globals, GlobalState{})
global_cells usize := 0
for (module.globals.items) |global| {
if (!ir.aggregate(global.type)) continue
cells :: module.arrays.items[usize(global.array_type)].cells
if (cells > maxval!(usize) - global_cells) return .storage_limit_reached
global_cells += cells
}
aggregate_globals :: try mem.alloc(ir.Value, allocator, global_cells)
defer if (!transferred) mem.free(allocator, aggregate_globals)
global_offset usize := 0
for (module.globals.items) |global, i| {
if (!ir.aggregate(global.type)) continue
cells :: module.arrays.items[usize(global.array_type)].cells
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 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
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 if (!transferred) mem.free(allocator, string_cells)
strings :: try mem.alloc(StringView, allocator, module.strings.items.len)
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]
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 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)
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 {
frame_index :: frames.items.len - 1
function :: &module.functions.items[usize(frames.items[frame_index].function)]
block :: &function.blocks.items[usize(frames.items[frame_index].block)]
instruction_index :: frames.items[frame_index].next_instruction
if $(bounded) {
budget :: remaining?
if (budget^ == 0 or frames.items.len > 128) {
span :: if (instruction_index < block.instructions.items.len)
block.instructions.items[instruction_index].span else block.terminator.span
return try runtime_trap(diagnostic_store, .comptime_execution_limit, span)
}
budget^ -= 1
}
if instruction_index == block.instructions.items.len {
terminator :: block.terminator
if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic }
if (terminator.kind == .unreachable) return try runtime_trap(diagnostic_store, .unreachable_executed, terminator.span)
if (terminator.kind == .jump or terminator.kind == .branch) {
target := terminator.target
if (terminator.kind == .branch) {
condition :: frames.items[frame_index].values[usize(terminator.value)]
if (!condition.data.boolean) target = terminator.alternative
}
frames.items[frame_index].block = target
frames.items[frame_index].next_instruction = 0
continue
}
value := ir.Value{}
if (terminator.value != ir.no_value) value = frames.items[frame_index].values[usize(terminator.value)]
completed :: frames.items[frame_index]
# transfer leaves while the completed frame still owns its backing.
if completed.global |global_index| {
copy_value(module, &globals[global_index].value, value)
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)
value.data.elements = escaped
return Outcome{ value = value, allocator = allocator, owned_elements = escaped }
}
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
}
instruction :: &block.instructions.items[instruction_index]
frames.items[frame_index].next_instruction += 1
value := ir.Value{}
match instruction.op {
.constant: value = instruction.constant
.undefined_value: {
destination :: &frames.items[frame_index].values[usize(instruction.destination)]
if (ir.aggregate(instruction.type)) {
mem.set(destination.data.elements, ir.Value{ type = .invalid, uninitialized = true })
} else destination^ = ir.Value{ type = instruction.type, uninitialized = true }
continue
}
.load_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
if (!local.initialized) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
value = local.value
}
.store_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
copy_value(module, &local.value, frames.items[frame_index].values[usize(instruction.left)])
local.initialized = true
continue
}
.assign_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
if (!local.initialized) return try runtime_trap(diagnostic_store, .write_before_initialization, instruction.span)
copy_value(module, &local.value, frames.items[frame_index].values[usize(instruction.left)])
continue
}
.reset_local: {
frames.items[frame_index].locals[usize(instruction.slot)].initialized = false
continue
}
.load_global: {
global_index :: usize(instruction.slot)
match globals[global_index].status {
.ready: value = globals[global_index].value
.initializing: {
return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span)
}
.uninitialized: {
try push_frame(
allocator,
module,
&frames,
module.globals.items[global_index].initializer,
instruction.destination,
global_index,
)
globals[global_index].status = .initializing
continue
}
}
}
.call: {
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| {
argument :: block.arguments.items[instruction.arguments_start + argument_index]
local :: &frames.items[frame_index + 1].locals[argument_index]
copy_value(module, &local.value, frames.items[frame_index].values[usize(argument)])
local.initialized = true
}
continue
}
.array_literal, .record_literal: {
destination :: &frames.items[frame_index].values[usize(instruction.destination)]
offset usize := 0
for (0..instruction.arguments_count) |i| {
operand :: block.arguments.items[instruction.arguments_start + i]
element :: frames.items[frame_index].values[usize(operand)]
if (ir.aggregate(element.type)) {
count :: element.data.elements.len
target :: aggregate_view(element.type, element.array_type, destination.data.elements[offset..offset + count])
ir.copy_aggregate(module, target, element)
offset += count
} else {
destination.data.elements[offset] = element
offset += 1
}
}
continue
}
.union_literal: {
destination :: frames.items[frame_index].values[usize(instruction.destination)]
shape :: module.arrays.items[usize(instruction.array_type)]
mem.set(destination.data.elements, ir.Value{ type = .u8, data = ir.ValueData{ unsigned = 0 } })
if (shape.tag_type != .void) ir.write_storage(module, destination.data.elements, instruction.constant)
if (instruction.left != ir.no_value) {
payload :: frames.items[frame_index].values[usize(instruction.left)]
ir.write_storage(module, destination.data.elements[shape.payload_offset..], payload)
}
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
shape :: module.arrays.items[usize(base.array_type)]
if (index >= u64(shape.len)) return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span)
value = ir.aggregate_item(module, base, usize(index))
}
.field: {
base :: frames.items[frame_index].values[usize(instruction.left)]
shape :: module.arrays.items[usize(base.array_type)]
if (shape.is_union and !ir.union_active(module, base, usize(instruction.slot))) return try runtime_trap(diagnostic_store, .inactive_union_field, instruction.span)
value = ir.aggregate_item(module, base, usize(instruction.slot))
}
.address_local: {
local :: &frames.items[frame_index].locals[usize(instruction.slot)]
address := Address{
type = instruction.pointee,
array_type = instruction.array_type,
cells = &[],
initialized = &local.initialized,
}
address.root = &local.value
address.root_type = address.type
address.root_array = address.array_type
if (ir.aggregate(address.type)) address.cells = local.value.data.elements
else address.scalar = &local.value
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{
type = .address,
data = ir.ValueData{ address = u32(instruction.destination) },
}
}
.address_global: {
global_index :: usize(instruction.slot)
match globals[global_index].status {
.initializing: return try runtime_trap(diagnostic_store, .initialization_cycle, instruction.span)
.uninitialized: {
frames.items[frame_index].next_instruction -= 1
try push_frame(allocator, module, &frames, module.globals.items[global_index].initializer, ir.no_value, global_index)
globals[global_index].status = .initializing
continue
}
.ready: {}
}
global :: &globals[global_index]
address := Address{
type = instruction.pointee,
array_type = instruction.array_type,
cells = &[],
}
address.root = &global.value
address.root_type = address.type
address.root_array = address.array_type
if (ir.aggregate(address.type)) address.cells = global.value.data.elements
else address.scalar = &global.value
frames.items[frame_index].addresses[usize(instruction.destination)] = address
value = ir.Value{
type = .address,
data = ir.ValueData{ address = u32(instruction.destination) },
}
}
.index_address: {
base_id :: frames.items[frame_index].values[usize(instruction.left)].data.address
base :: frames.items[frame_index].addresses[usize(base_id)]
index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
shape :: module.arrays.items[usize(base.array_type)]
if (index >= u64(shape.len)) return try runtime_trap(diagnostic_store, .index_out_of_bounds, instruction.span)
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
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 !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) } }
}
.field_address: {
base_id :: frames.items[frame_index].values[usize(instruction.left)].data.address
base :: frames.items[frame_index].addresses[usize(base_id)]
shape :: module.arrays.items[usize(base.array_type)]
field :: shape.fields[usize(instruction.slot)]
if (shape.is_union and shape.tag_type != .void) {
if base.initialized |initialized| {
if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
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 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
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 !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) } }
}
.pointer_from_address: {
id :: frames.items[frame_index].values[usize(instruction.left)].data.address
address :: frames.items[frame_index].addresses[usize(id)]
if address.initialized |initialized| {
if (!initialized^) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
}
value = try pointer_value(&pointers, Pointer{
root = address.root?,
root_type = address.root_type,
root_array = address.root_array,
byte_offset = address.byte_offset,
readonly = address.readonly,
bytes = address.bytes,
})
}
.pointer_address: {
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)
location :: pointers.items[usize(handle)]
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 {
return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
}
}
.pointer_offset: {
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 := pointers.items[usize(handle)]
index :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
stride :: ir.storage_size(module, instruction.pointee, instruction.array_type)
if (stride != 0 and index > u64(divtrunc!(maxval!(usize) - address.byte_offset, stride))) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
address.byte_offset += usize(index) * stride
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)
}
.load_address, .store_address: {
id :: frames.items[frame_index].values[usize(instruction.left)].data.address
address :: frames.items[frame_index].addresses[usize(id)]
if address.initialized |initialized| {
if (!initialized^) {
code :: if (instruction.op == .load_address) diagnostics.Code.read_before_initialization else diagnostics.Code.write_before_initialization
return try runtime_trap(diagnostic_store, code, instruction.span)
}
}
if (instruction.op == .load_address) {
value = read_address(module, address, scratch)
} else {
source :: frames.items[frame_index].values[usize(instruction.right)]
if (address.readonly) return try runtime_trap(diagnostic_store, .invalid_pointer_access, instruction.span)
write_address(module, address, source, scratch)
continue
}
}
.convert: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
if (convert(operand, instruction.type)) |converted| value = converted
else return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
.negate: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
if instruction.type == .float {
value = ir.Value{ type = .float, data = ir.ValueData{ real = -operand.data.real } }
} else {
if (operand.data.integer == enummap.get(&ir.integer_minima, instruction.type)?) return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
value = ir.Value{ type = instruction.type, data = ir.ValueData{ integer = -operand.data.integer } }
}
}
.logical_not: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = !operand.data.boolean } }
}
.bit_not: {
operand :: frames.items[frame_index].values[usize(instruction.left)]
mask :: maxval!(u64) >> u64(64 - ir.integer_bits(instruction.type))
value = integer_from_bits(instruction.type, operand.data.unsigned xor mask)
}
.bit_and, .bit_or, .bit_xor: {
left :: frames.items[frame_index].values[usize(instruction.left)].data.unsigned
right :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
bits u64 :: match instruction.op {
.bit_and: left & right
.bit_or: left | right
.bit_xor: left xor right
else: unreachable
}
value = integer_from_bits(instruction.type, bits)
}
.shift_left, .shift_right, .shift_left_saturating: {
left :: frames.items[frame_index].values[usize(instruction.left)]
count :: frames.items[frame_index].values[usize(instruction.right)].data.unsigned
if (instruction.op != .shift_left_saturating and count >= u64(ir.integer_bits(instruction.type)))
return try runtime_trap(diagnostic_store, .invalid_shift_count, instruction.span)
value = shift_integer(instruction.op, left, count)
}
.add, .subtract, .multiply, .divide, .remainder: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
if ((instruction.op == .divide or instruction.op == .remainder) and instruction.type != .float and right.data.unsigned == 0)
return try runtime_trap(diagnostic_store, .division_by_zero, instruction.span)
if instruction.type == .float {
if (instruction.op == .remainder and right.data.real == 0)
return try runtime_trap(diagnostic_store, .division_by_zero, instruction.span)
result f64 := 0
match instruction.op {
.add: result = left.data.real + right.data.real
.subtract: result = left.data.real - right.data.real
.multiply: result = left.data.real * right.data.real
.divide: result = left.data.real / right.data.real
.remainder: result = rem!(left.data.real, right.data.real)
else: unreachable
}
value = ir.Value{ type = .float, data = ir.ValueData{ real = result } }
} else if ir.unsigned_integer(instruction.type) {
result :: checked_unsigned(instruction.op, left.data.unsigned, right.data.unsigned)
if result |unsigned| {
if (unsigned > enummap.get(&ir.integer_maxima, instruction.type)?) return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
value = ir.Value{ type = instruction.type, data = ir.ValueData{ unsigned = unsigned } }
} else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
} else {
result :: checked_integer(instruction.op, left.data.integer, right.data.integer)
if result |result_integer| {
if (result_integer < enummap.get(&ir.integer_minima, instruction.type)? or result_integer > i64(enummap.get(&ir.integer_maxima, instruction.type)?)) return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
value = ir.Value{ type = instruction.type, data = ir.ValueData{ integer = result_integer } }
} else {
return try runtime_trap(diagnostic_store, .integer_overflow, instruction.span)
}
}
}
.equal, .not_equal, .less, .less_equal, .greater, .greater_equal: {
left :: frames.items[frame_index].values[usize(instruction.left)]
right :: frames.items[frame_index].values[usize(instruction.right)]
result := false
match left.type {
.int, .isize, .i8, .i16, .i32: result = match instruction.op {
.equal: left.data.integer == right.data.integer
.not_equal: left.data.integer != right.data.integer
.less: left.data.integer < right.data.integer
.less_equal: left.data.integer <= right.data.integer
.greater: left.data.integer > right.data.integer
.greater_equal: left.data.integer >= right.data.integer
else: unreachable
}
.usize, .u8, .u16, .u32, .u64: result = match instruction.op {
.equal: left.data.unsigned == right.data.unsigned
.not_equal: left.data.unsigned != right.data.unsigned
.less: left.data.unsigned < right.data.unsigned
.less_equal: left.data.unsigned <= right.data.unsigned
.greater: left.data.unsigned > right.data.unsigned
.greater_equal: left.data.unsigned >= right.data.unsigned
else: unreachable
}
.float: result = match instruction.op {
.equal: left.data.real == right.data.real
.not_equal: left.data.real != right.data.real
.less: left.data.real < right.data.real
.less_equal: left.data.real <= right.data.real
.greater: left.data.real > right.data.real
.greater_equal: left.data.real >= right.data.real
else: unreachable
}
.bool: result = match instruction.op {
.equal: left.data.boolean == right.data.boolean
.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
else: unreachable
}
else: unreachable
}
value = ir.Value{ type = .bool, data = ir.ValueData{ boolean = result } }
}
}
if (value.uninitialized) return try runtime_trap(diagnostic_store, .read_before_initialization, instruction.span)
if (instruction.destination != ir.no_value) copy_value(module, &frames.items[frame_index].values[usize(instruction.destination)], value)
}
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, raw = pointer.root^.raw,
}
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.
root_bits :: ptrcast!(@usize, &address.root)^
for (pointers.items) |location, handle| {
if (ptrcast!(@usize, &location.root)^ == root_bits and location.byte_offset == address.byte_offset) {
# refresh metadata if a later invocation reused a released frame's allocation.
pointers.items[handle] = address
return ir.Value{ type = .pointer, data = ir.ValueData{ unsigned = u64(handle) } }
}
}
handle :: pointers.items.len
try arraylist.append(pointers, address)
return ir.Value{ type = .pointer, data = ir.ValueData{ unsigned = u64(handle) } }
}
@hide:file
project_pointer proc(
module @ir.Module,
base Address,
offset usize,
type ir.Type,
array_type ir.ArrayId,
) ?Address {
if (offset == 0 and base.type == type and base.array_type == array_type) return base
if (!ir.aggregate(base.type)) return null
shape :: module.arrays.items[usize(base.array_type)]
if (base.raw or shape.is_union) {
width :: ir.storage_size(module, type, array_type)
if (offset > base.cells.len or width > base.cells.len - offset) return null
address := base
address.type = type
address.array_type = array_type
address.cells = base.cells[offset..offset + width]
address.scalar = null
address.raw = true
address.byte_offset += offset
return address
}
if (shape.kind == .array) {
stride :: ir.storage_size(module, shape.element, shape.element_array)
index :: if (stride == 0) usize(0) else divtrunc!(offset, stride)
if (index >= shape.len) return null
width :: element_cells(module, shape)
start :: index * width
child := base
child.type = shape.element
child.array_type = shape.element_array
child.cells = base.cells[start..start + width]
child.scalar = null
child.byte_offset += index * stride
if (!ir.aggregate(child.type)) child.scalar = &child.cells[0]
return project_pointer(module, child, offset - index * stride, type, array_type)
}
for (shape.fields) |field| {
if (offset < field.byte_offset) continue
relative :: offset - field.byte_offset
if (relative > ir.storage_size(module, field.type, field.array_type)) continue
width :: if (ir.aggregate(field.type)) module.arrays.items[usize(field.array_type)].cells else 1
child := base
child.type = field.type
child.array_type = field.array_type
child.cells = base.cells[field.offset..field.offset + width]
child.scalar = null
child.byte_offset += field.byte_offset
if (!ir.aggregate(child.type)) child.scalar = &child.cells[0]
if (project_pointer(module, child, relative, type, array_type)) |address| return address
}
return null
}
@hide:file
push_frame proc(
allocator mem.Allocator,
module @ir.Module,
frames @mut arraylist.ArrayList(Frame),
function ir.FunctionId,
destination ir.ValueId,
global ?usize,
) void ! mem.AllocError {
signature :: &module.functions.items[usize(function)]
locals :: try mem.alloc(Slot, allocator, signature.locals.items.len)
errdefer mem.free(allocator, locals)
mem.set(locals, Slot{})
values :: try mem.alloc(ir.Value, allocator, signature.value_count)
errdefer mem.free(allocator, values)
aggregate :: try mem.alloc(ir.Value, allocator, signature.aggregate_cells)
errdefer mem.free(allocator, aggregate)
address_count usize :: if (signature.has_addresses) signature.value_count else 0
addresses :: try mem.alloc(Address, allocator, address_count)
errdefer mem.free(allocator, addresses)
for (signature.array_storage.items) |storage| {
cells :: module.arrays.items[usize(storage.array_type)].cells
value :: aggregate_view(module.arrays.items[usize(storage.array_type)].kind, storage.array_type, aggregate[storage.offset..storage.offset + cells])
if (storage.local) locals[usize(storage.index)].value = value
else values[usize(storage.index)] = value
}
# typed IR assigns each temporary before use; only local slots need initialization flags.
try arraylist.append(frames, Frame{
function = function,
block = signature.entry,
destination = destination,
global = global,
locals = locals,
values = values,
aggregate = aggregate,
addresses = addresses,
})
}
@hide:file
free_frame proc(allocator mem.Allocator, frame Frame) void {
mem.free(allocator, frame.locals)
mem.free(allocator, frame.values)
mem.free(allocator, frame.aggregate)
mem.free(allocator, frame.addresses)
}
@hide:file
runtime_trap proc(
diagnostic_store @mut diagnostics.Store,
code diagnostics.Code,
span diagnostics.Span,
) Outcome ! (mem.AllocError | diagnostics.CapacityError) {
return Outcome{ trap = try diagnostics.add(diagnostic_store, code, span) }
}
@hide:file
integer_from_bits proc(type ir.Type, bits u64) ir.Value {
data := ir.ValueData{ unsigned = bits }
match type {
.i8: data = ir.ValueData{ integer = i64(ptrcast!(@i8, &bits)^) }
.i16: data = ir.ValueData{ integer = i64(ptrcast!(@i16, &bits)^) }
.i32: data = ir.ValueData{ integer = i64(ptrcast!(@i32, &bits)^) }
.int, .isize: data = ir.ValueData{ integer = ptrcast!(@i64, &bits)^ }
.u8: data = ir.ValueData{ unsigned = u64(ptrcast!(@u8, &bits)^) }
.u16: data = ir.ValueData{ unsigned = u64(ptrcast!(@u16, &bits)^) }
.u32: data = ir.ValueData{ unsigned = u64(ptrcast!(@u32, &bits)^) }
.u64, .usize: {}
else: unreachable
}
return ir.Value{ type = type, data = data }
}
@hide:file
shift_integer proc(op ir.Op, left ir.Value, count u64) ir.Value {
width :: u64(ir.integer_bits(left.type))
mask :: maxval!(u64) >> (64 - width)
bits :: left.data.unsigned & mask
signed :: !ir.unsigned_integer(left.type)
negative :: signed and left.data.integer < 0
if (op == .shift_left_saturating) {
if (bits == 0) return left
limit :: if (negative) 1 + (mask >> 1) else enummap.get(&ir.integer_maxima, left.type)?
magnitude :: if (negative) 1 + (bits xor mask) else bits
if (count >= width) return integer_from_bits(left.type, limit)
if (magnitude > (limit >> count)) return integer_from_bits(left.type, limit)
}
if (op == .shift_right) {
shifted := bits >> count
if (negative) shifted = shifted | (mask xor (mask >> count))
return integer_from_bits(left.type, shifted)
}
# discard high bits before shifting so the bootstrap's checked arithmetic cannot overflow.
return integer_from_bits(left.type, (bits & (mask >> count)) << count)
}
@hide:file
checked_integer proc(op ir.Op, left i64, right i64) ?i64 {
minimum :: minval!(i64)
maximum :: maxval!(i64)
match op {
.add: {
if (right > 0 and left > maximum - right) return null
if (right < 0 and left < minimum - right) return null
return left + right
}
.subtract: {
if (right > 0 and left < minimum + right) return null
if (right < 0 and left > maximum + right) return null
return left - right
}
.multiply: {
if left > 0 {
if (right > 0 and left > divtrunc!(maximum, right)) return null
if (right < 0 and right < divtrunc!(minimum, left)) return null
} else if left < 0 {
if (right > 0 and left < divtrunc!(minimum, right)) return null
if (right < 0 and left < divtrunc!(maximum, right)) return null
}
return left * right
}
.divide: {
if (left == minimum and right == -1) return null
return divtrunc!(left, right)
}
.remainder: return rem!(left, right)
else: unreachable
}
}
@hide:file
aggregate_view proc(type ir.Type, shape ir.ArrayId, cells []mut ir.Value) ir.Value {
return ir.Value{ type = type, array_type = shape, data = ir.ValueData{ elements = cells } }
}
@hide:file
copy_value proc(module @ir.Module, destination @mut ir.Value, source ir.Value) void {
if (ir.aggregate(source.type)) {
# aggregate destinations already own disjoint fixed segments; never install an alias.
ir.copy_aggregate(module, destination^, source)
} else {
destination^ = source
}
}
@hide:file
element_cells proc(module @ir.Module, shape ir.ArrayType) usize {
return if (ir.aggregate(shape.element)) module.arrays.items[usize(shape.element_array)].cells else 1
}
@hide:file
checked_unsigned proc(op ir.Op, left u64, right u64) ?u64 {
match op {
.add: {
if (left > maxval!(u64) - right) return null
return left + right
}
.subtract: {
if (left < right) return null
return left - right
}
.multiply: {
if (right != 0 and left > divtrunc!(maxval!(u64), right)) return null
return left * right
}
.divide: return divtrunc!(left, right)
.remainder: return rem!(left, right)
else: unreachable
}
}
@hide:file
convert proc(source ir.Value, target ir.Type) ?ir.Value {
result := ir.Value{ type = target }
if (target == .float) {
if (source.type == .float) result.data = source.data
else if (ir.unsigned_integer(source.type)) result.data = ir.ValueData{ real = f64(source.data.unsigned) }
else result.data = ir.ValueData{ real = f64(source.data.integer) }
return result
}
if (source.type == .float) {
real :: source.data.real
# Guard the conversion itself before testing narrower bounds; NaN fails
# ordered comparisons, and the upper 64-bit bound is exclusive.
if (ir.unsigned_integer(target)) {
if (!(real > -1.0 and real < 18446744073709551616.0)) return null
integer :: u64(real)
if (integer > enummap.get(&ir.integer_maxima, target)?) return null
result.data = ir.ValueData{ unsigned = integer }
} else {
if (!(real >= -9223372036854775808.0 and real < 9223372036854775808.0)) return null
integer :: i64(real)
if (integer < enummap.get(&ir.integer_minima, target)? or integer > i64(enummap.get(&ir.integer_maxima, target)?)) return null
result.data = ir.ValueData{ integer = integer }
}
return result
}
if (ir.unsigned_integer(source.type)) {
integer :: source.data.unsigned
if (integer > enummap.get(&ir.integer_maxima, target)?) return null
if (ir.unsigned_integer(target)) result.data = ir.ValueData{ unsigned = integer }
else result.data = ir.ValueData{ integer = i64(integer) }
} else {
integer :: source.data.integer
if (ir.unsigned_integer(target)) {
if (integer < 0 or u64(integer) > enummap.get(&ir.integer_maxima, target)?) return null
result.data = ir.ValueData{ unsigned = u64(integer) }
} else {
if (integer < enummap.get(&ir.integer_minima, target)? or integer > i64(enummap.get(&ir.integer_maxima, target)?)) return null
result.data = ir.ValueData{ integer = integer }
}
}
return result
}