import "@std/mem" import "@std/arraylist" 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 } #! 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. deinit proc(outcome @mut Outcome) void { if (outcome.owned_elements) |elements| mem.free(outcome.allocator?, elements) 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 } #! 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) { entry ir.FunctionId := ir.FunctionId(0) if module.entry |function| { entry = function } else { return Outcome{ trap = module.entry_diagnostic } } globals :: try mem.alloc(GlobalState, allocator, module.globals.items.len) defer 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 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 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) 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 instruction_index == block.instructions.items.len { terminator :: block.terminator if (terminator.kind == .trap) return Outcome{ trap = terminator.diagnostic } if (terminator.kind == .unreachable) unreachable 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 (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) } _ = 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 .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 == ir.integer_min(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 } } } .add, .subtract, .multiply, .divide: { left :: frames.items[frame_index].values[usize(instruction.left)] right :: frames.items[frame_index].values[usize(instruction.right)] if instruction.type == .float { 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 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 > ir.integer_max(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 < ir.integer_min(instruction.type) or result_integer > i64(ir.integer_max(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 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, } 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 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 } 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 } 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 > ir.integer_max(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 < ir.integer_min(target) or integer > i64(ir.integer_max(target))) return null result.data = ir.ValueData{ integer = integer } } return result } if (ir.unsigned_integer(source.type)) { integer :: source.data.unsigned if (integer > ir.integer_max(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) > ir.integer_max(target)) return null result.data = ir.ValueData{ unsigned = u64(integer) } } else { if (integer < ir.integer_min(target) or integer > i64(ir.integer_max(target))) return null result.data = ir.ValueData{ integer = integer } } } return result }