import "@std/mem" import "@std/arraylist" import "@std/testing" import "@source/ir" import "@source/diagnostics" @hide:file add_function proc( module @mut ir.Module, locals []ir.Type, parameter_count usize, result ir.Type, value_count usize, ) ir.FunctionId ! mem.AllocError { function := ir.init_function(module.allocator) errdefer { arraylist.deinit(&function.parameters) arraylist.deinit(&function.locals) for (function.blocks.items) |block| { owned := block arraylist.deinit(&owned.instructions) arraylist.deinit(&owned.arguments) } arraylist.deinit(&function.blocks) } for (locals) |type, index| { try arraylist.append(&function.locals, type) if (index < parameter_count) try arraylist.append(&function.parameters, type) } function.result = result function.value_count = value_count try arraylist.append(&function.blocks, ir.init_block(module.allocator)) id :: ir.FunctionId(module.functions.items.len) try arraylist.append(&module.functions, function) return id } @hide:file emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError { try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction) } @hide:file return_value proc(module @mut ir.Module, function ir.FunctionId, value ir.ValueId) void { module.functions.items[usize(function)].blocks.items[0].terminator = ir.Terminator{ value = value } } @hide:file integer proc(destination u32, value i64) ir.Instruction { return ir.Instruction{ op = .constant, type = .int, destination = ir.ValueId(destination), constant = ir.Value{ type = .int, data = ir.ValueData{ integer = value } }, } } @hide:file function_value proc(destination u32, function ir.FunctionId) ir.Instruction { return ir.Instruction{ op = .constant, type = .function, destination = ir.ValueId(destination), constant = ir.Value{ type = .function, data = ir.ValueData{ function = function } }, } } @hide:file expect_trap proc( outcome Outcome, store @diagnostics.Store, code diagnostics.Code, span diagnostics.Span, ) void ! testing.Error { if outcome.trap |id| { if diagnostics.get(store, id) |diagnostic| { try testing.expect_equal(code, diagnostic.code) try testing.expect_equal(span.start, diagnostic.span.start) try testing.expect_equal(span.end, diagnostic.span.end) return } } return .expectation_failed } repeated_calls_have_fresh_parameters_locals_and_temporaries test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [2]ir.Type :: [.int, .int] bump :: try add_function(&module, types[..], 1, .int, 4) try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 }) try emit(&module, bump, integer(1, 1)) try emit(&module, bump, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) try emit(&module, bump, ir.Instruction{ op = .store_local, left = ir.ValueId(2), slot = 1 }) try emit(&module, bump, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(3), slot = 1 }) return_value(&module, bump, ir.ValueId(3)) noop :: try add_function(&module, mem.empty(ir.Type), 0, .void, 0) main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 8) module.entry = main try emit(&module, main, function_value(0, bump)) try emit(&module, main, integer(1, 19)) try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1)) try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 }) try emit(&module, main, integer(3, 21)) try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(3)) try emit(&module, main, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), arguments_start = 1, arguments_count = 1 }) try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(5), left = ir.ValueId(2), right = ir.ValueId(4) }) try emit(&module, main, function_value(6, noop)) try emit(&module, main, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(7), left = ir.ValueId(6) }) return_value(&module, main, ir.ValueId(5)) outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(usize(0), diagnostics.count(&store)) } lazy_globals_follow_forward_dependencies_and_reset_between_runs test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3) second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 }) try emit(&module, first, integer(1, 1)) try emit(&module, first, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) return_value(&module, first, ir.ValueId(2)) try emit(&module, second, integer(0, 20)) return_value(&module, second, ir.ValueId(0)) try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } }) try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } }) main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3) module.entry = main try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(1), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) return_value(&module, main, ir.ValueId(2)) initial :: try run(mem.c_allocator, &module, &store) try testing.expect(initial.trap == null) try testing.expect_equal(i64(42), initial.value.data.integer) module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 30) repeated :: try run(mem.c_allocator, &module, &store) try testing.expect(repeated.trap == null) try testing.expect_equal(i64(62), repeated.value.data.integer) } function_aliases_and_borrowed_string_values_execute test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) text :: "line\n\"quoted\"\x00" owned :: try mem.alloc(u8, module.allocator, text.len) transferred := false defer if (!transferred) mem.free(module.allocator, owned) memcopy!(owned, text) try arraylist.append(&module.strings, ir.StringStorage{ allocation = owned, len = text.len }) transferred = true target :: try add_function(&module, mem.empty(ir.Type), 0, .string, 1) try emit(&module, target, ir.Instruction{ op = .constant, type = .string, destination = ir.ValueId(0), constant = ir.Value{ type = .string, data = ir.ValueData{ string = ir.StringId(0) } }, }) return_value(&module, target, ir.ValueId(0)) initializer :: try add_function(&module, mem.empty(ir.Type), 0, .function, 1) try emit(&module, initializer, function_value(0, target)) return_value(&module, initializer, ir.ValueId(0)) try arraylist.append(&module.globals, ir.Global{ type = .function, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } }) main :: try add_function(&module, mem.empty(ir.Type), 0, .string, 2) module.entry = main try emit(&module, main, ir.Instruction{ op = .load_global, type = .function, destination = ir.ValueId(0), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .call, type = .string, destination = ir.ValueId(1), left = ir.ValueId(0) }) return_value(&module, main, ir.ValueId(1)) outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.string, outcome.value.type) try testing.expect_equal([]u8, text, ir.string_bytes(&module, outcome.value.data.string)) } premature_local_reads_and_global_cycles_report_instruction_spans test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) span :: diagnostics.Span{ start = 14, end = 19 } types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 1) module.entry = main try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0, span = span }) return_value(&module, main, ir.ValueId(0)) premature :: try run(mem.c_allocator, &module, &store) try expect_trap(premature, &store, .read_before_initialization, span) first :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) second :: try add_function(&module, mem.empty(ir.Type), 0, .int, 1) cycle_span :: diagnostics.Span{ start = 40, end = 44 } try emit(&module, first, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 1 }) try emit(&module, second, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0, span = cycle_span }) return_value(&module, first, ir.ValueId(0)) return_value(&module, second, ir.ValueId(0)) try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = first, span = diagnostics.Span{ start = 0, end = 0 } }) try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = second, span = diagnostics.Span{ start = 0, end = 0 } }) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 } cycle :: try run(mem.c_allocator, &module, &store) try expect_trap(cycle, &store, .initialization_cycle, cycle_span) retry :: try run(mem.c_allocator, &module, &store) try expect_trap(retry, &store, .initialization_cycle, cycle_span) } existing_trap_identity_entry_failures_and_unused_errors_survive test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) entry_id :: try diagnostics.add(&store, .missing_entry, diagnostics.Span{ start = 0, end = 0 }) module.entry_diagnostic = entry_id absent :: try run(mem.c_allocator, &module, &store) try testing.expect_equal(entry_id, absent.trap?) invalid_id :: try diagnostics.add(&store, .unknown_name, diagnostics.Span{ start = 12, end = 17 }) bad :: try add_function(&module, mem.empty(ir.Type), 0, .invalid, 0) module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id } main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2) module.entry = main try emit(&module, main, integer(0, 42)) return_value(&module, main, ir.ValueId(0)) success :: try run(mem.c_allocator, &module, &store) try testing.expect(success.trap == null) try testing.expect_equal(i64(42), success.value.data.integer) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = function_value(0, bad) try emit(&module, main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) }) trapped :: try run(mem.c_allocator, &module, &store) try testing.expect_equal(invalid_id, trapped.trap?) try testing.expect_equal(usize(2), diagnostics.count(&store)) } @hide:file IntegerCase :: struct { op ir.Op left i64 right i64 expected ?i64 } integer_arithmetic_checks_boundaries_before_host_operations test { cases [19]IntegerCase :: [ IntegerCase{ op = .add, left = 19, right = 23, expected = 42 }, IntegerCase{ op = .add, left = maxval!(i64), right = 0, expected = maxval!(i64) }, IntegerCase{ op = .add, left = maxval!(i64), right = 1, expected = null }, IntegerCase{ op = .add, left = minval!(i64), right = -1, expected = null }, IntegerCase{ op = .add, left = minval!(i64), right = maxval!(i64), expected = -1 }, IntegerCase{ op = .subtract, left = minval!(i64), right = 0, expected = minval!(i64) }, IntegerCase{ op = .subtract, left = minval!(i64), right = 1, expected = null }, IntegerCase{ op = .subtract, left = maxval!(i64), right = -1, expected = null }, IntegerCase{ op = .subtract, left = -1, right = minval!(i64), expected = maxval!(i64) }, IntegerCase{ op = .multiply, left = minval!(i64), right = 1, expected = minval!(i64) }, IntegerCase{ op = .multiply, left = minval!(i64), right = -1, expected = null }, IntegerCase{ op = .multiply, left = -1, right = minval!(i64), expected = null }, IntegerCase{ op = .multiply, left = maxval!(i64), right = 2, expected = null }, IntegerCase{ op = .multiply, left = minval!(i64), right = 2, expected = null }, IntegerCase{ op = .multiply, left = -3, right = -14, expected = 42 }, IntegerCase{ op = .multiply, left = -3, right = 14, expected = -42 }, IntegerCase{ op = .multiply, left = 3, right = -14, expected = -42 }, IntegerCase{ op = .multiply, left = minval!(i64), right = 0, expected = 0 }, IntegerCase{ op = .multiply, left = 0, right = minval!(i64), expected = 0 }, ] for (cases) |case| { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) span :: diagnostics.Span{ start = 8, end = 16 } main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3) module.entry = main try emit(&module, main, integer(0, case.left)) try emit(&module, main, integer(1, case.right)) try emit(&module, main, ir.Instruction{ op = case.op, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1), span = span }) return_value(&module, main, ir.ValueId(2)) outcome :: try run(mem.c_allocator, &module, &store) if case.expected |expected| { try testing.expect(outcome.trap == null) try testing.expect_equal(expected, outcome.value.data.integer) } else { try expect_trap(outcome, &store, .integer_overflow, span) } } } integer_negation_traps_only_at_minimum test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) span :: diagnostics.Span{ start = 21, end = 27 } main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 2) module.entry = main try emit(&module, main, integer(0, minval!(i64))) try emit(&module, main, ir.Instruction{ op = .negate, type = .int, destination = ir.ValueId(1), left = ir.ValueId(0), span = span }) return_value(&module, main, ir.ValueId(1)) trapped :: try run(mem.c_allocator, &module, &store) try expect_trap(trapped, &store, .integer_overflow, span) module.functions.items[usize(main)].blocks.items[0].instructions.items[0] = integer(0, maxval!(i64)) success :: try run(mem.c_allocator, &module, &store) try testing.expect(success.trap == null) try testing.expect_equal(-maxval!(i64), success.value.data.integer) } float_arithmetic_and_ieee_zero_division_execute test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) main :: try add_function(&module, mem.empty(ir.Type), 0, .float, 7) module.entry = main try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 10.5 } } }) try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(1), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 2.0 } } }) try emit(&module, main, ir.Instruction{ op = .multiply, type = .float, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .add, type = .float, destination = ir.ValueId(3), left = ir.ValueId(2), right = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .subtract, type = .float, destination = ir.ValueId(4), left = ir.ValueId(3), right = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(5), left = ir.ValueId(4), right = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .negate, type = .float, destination = ir.ValueId(6), left = ir.ValueId(5) }) return_value(&module, main, ir.ValueId(6)) result :: try run(mem.c_allocator, &module, &store) try testing.expect(result.trap == null) try testing.expect_equal(ir.Type.float, result.value.type) try testing.expect_equal(f64(-10.5), result.value.data.real) module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 0 module.functions.items[usize(main)].blocks.items[0].instructions.items[1].constant.data.real = 0 nan :: try run(mem.c_allocator, &module, &store) try testing.expect(nan.trap == null) try testing.expect(nan.value.data.real != nan.value.data.real) module.functions.items[usize(main)].blocks.items[0].instructions.items[0].constant.data.real = 1 module.functions.items[usize(main)].blocks.items[0].instructions.items[2].op = .add infinite :: try run(mem.c_allocator, &module, &store) try testing.expect(infinite.trap == null) try testing.expect(infinite.value.data.real < f64(-1000000000)) try testing.expect_equal(infinite.value.data.real, infinite.value.data.real * f64(2)) try testing.expect_equal(usize(0), diagnostics.count(&store)) } @hide:file AllocationState :: struct { remaining usize live usize = 0 } @hide:file limited_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { if context |userdata| { state @mut AllocationState :: ptrcast!(userdata) if (state.remaining == 0) return null state.remaining -= 1 memory :: mem.raw_alloc(mem.c_allocator, size, alignment) if (memory) |_| state.live += 1 return memory } unreachable } @hide:file limited_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { if context |userdata| { state @mut AllocationState :: ptrcast!(userdata) if (state.remaining == 0) return null state.remaining -= 1 resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment) if resized |_| { if (memory) |_| {} else state.live += 1 } return resized } unreachable } @hide:file limited_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void { if context |userdata| { state @mut AllocationState :: ptrcast!(userdata) if (memory) |_| state.live -= 1 mem.raw_free(mem.c_allocator, memory, size, alignment) return } unreachable } @hide:file limited_vtable :: mem.AllocatorVTable{ alloc = limited_alloc, realloc = limited_realloc, free = limited_free } recursive_invocations_use_heap_frames_and_free_on_allocation_failure test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) main :: try add_function(&module, mem.empty(ir.Type), 0, .void, 2) module.entry = main recursive_block :: try add_block(&module, main) module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = recursive_block } try emit_at(&module, main, recursive_block, function_value(0, main)) try emit_at(&module, main, recursive_block, ir.Instruction{ op = .call, type = .void, destination = ir.ValueId(1), left = ir.ValueId(0) }) state := AllocationState{ remaining = 2048 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } _ = run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed } try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), diagnostics.count(&store)) return } return .expectation_failed } frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] initializer :: try add_function(&module, types[..], 0, .int, 1) init_block :: try add_block(&module, initializer) module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = init_block } try emit_at(&module, initializer, init_block, integer(0, 42)) module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ value = ir.ValueId(0) } try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } }) main :: try add_function(&module, types[..], 0, .int, 1) module.entry = main work :: try add_block(&module, main) module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work } try emit_at(&module, main, work, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }) module.functions.items[usize(main)].blocks.items[usize(work)].terminator = ir.Terminator{ value = ir.ValueId(0) } succeeded := false failures usize := 0 for (usize(0)..usize(16)) |limit| { state := AllocationState{ remaining = limit } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } outcome :: run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed } failures += 1 try testing.expect_equal(usize(0), state.live) continue } succeeded = true try testing.expect(outcome.trap == null) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(usize(0), state.live) } try testing.expect(succeeded) try testing.expect(failures > 0) trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 }) module.functions.items[usize(initializer)].blocks.items[usize(init_block)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } trapped :: try run(allocator, &module, &store) try testing.expect_equal(trap_id, trapped.trap?) try testing.expect_equal(usize(0), state.live) } runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 1) module.entry = main try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 }) return_value(&module, main, ir.ValueId(0)) diagnostic_state := AllocationState{ remaining = 0 } diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable } store := diagnostics.init(diagnostic_allocator) defer diagnostics.deinit(&store) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } _ = run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed } try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), diagnostic_state.live) try testing.expect_equal(usize(0), diagnostics.count(&store)) return } return .expectation_failed } @hide:file add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.AllocError { body := &module.functions.items[usize(function)] id :: ir.BlockId(body.blocks.items.len) try arraylist.append(&body.blocks, ir.init_block(module.allocator)) return id } @hide:file emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError { try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction) } branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] count :: try add_function(&module, types[..], 1, .int, 9) done :: try add_block(&module, count) recurse :: try add_block(&module, count) try emit(&module, count, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 }) try emit(&module, count, integer(1, 0)) try emit(&module, count, ir.Instruction{ op = .less_equal, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) module.functions.items[usize(count)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(2), target = done, alternative = recurse, } module.functions.items[usize(count)].blocks.items[usize(done)].terminator = ir.Terminator{ value = ir.ValueId(1) } try emit_at(&module, count, recurse, function_value(3, count)) try emit_at(&module, count, recurse, integer(4, 1)) try emit_at(&module, count, recurse, ir.Instruction{ op = .subtract, type = .int, destination = ir.ValueId(5), left = ir.ValueId(0), right = ir.ValueId(4) }) try arraylist.append(&module.functions.items[usize(count)].blocks.items[usize(recurse)].arguments, ir.ValueId(5)) try emit_at(&module, count, recurse, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(6), left = ir.ValueId(3), arguments_count = 1 }) try emit_at(&module, count, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(7), left = ir.ValueId(6), right = ir.ValueId(4) }) module.functions.items[usize(count)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(7) } initializer :: try add_function(&module, mem.empty(ir.Type), 0, .int, 3) init_entry :: try add_block(&module, initializer) init_exit :: try add_block(&module, initializer) trap_id :: try diagnostics.add(&store, .invalid_escape, diagnostics.Span{ start = 0, end = 0 }) module.functions.items[usize(initializer)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } module.functions.items[usize(initializer)].entry = init_entry try emit_at(&module, initializer, init_entry, function_value(0, count)) try emit_at(&module, initializer, init_entry, integer(1, 128)) try arraylist.append(&module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].arguments, ir.ValueId(1)) try emit_at(&module, initializer, init_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(2), left = ir.ValueId(0), arguments_count = 1 }) module.functions.items[usize(initializer)].blocks.items[usize(init_entry)].terminator = ir.Terminator{ kind = .jump, target = init_exit } module.functions.items[usize(initializer)].blocks.items[usize(init_exit)].terminator = ir.Terminator{ value = ir.ValueId(2) } try arraylist.append(&module.globals, ir.Global{ type = .int, initializer = initializer, span = diagnostics.Span{ start = 0, end = 0 } }) main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5) main_entry :: try add_block(&module, main) main_exit :: try add_block(&module, main) module.entry = main module.functions.items[usize(main)].entry = main_entry module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } try emit_at(&module, main, main_entry, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }) try emit_at(&module, main, main_entry, function_value(1, count)) try emit_at(&module, main, main_entry, integer(2, 2)) try arraylist.append(&module.functions.items[usize(main)].blocks.items[usize(main_entry)].arguments, ir.ValueId(2)) try emit_at(&module, main, main_entry, ir.Instruction{ op = .call, type = .int, destination = ir.ValueId(3), left = ir.ValueId(1), arguments_count = 1 }) module.functions.items[usize(main)].blocks.items[usize(main_entry)].terminator = ir.Terminator{ kind = .jump, target = main_exit } try emit_at(&module, main, main_exit, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(4), left = ir.ValueId(0), right = ir.ValueId(3) }) module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ value = ir.ValueId(4) } state := AllocationState{ remaining = 2048 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } outcome :: try run(allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(i64(130), outcome.value.data.integer) try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(1), diagnostics.count(&store)) # trapping after returning through many heap frames also releases every live frame. module.functions.items[usize(main)].blocks.items[usize(main_exit)].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } trapped :: try run(allocator, &module, &store) try testing.expect_equal(trap_id, trapped.trap?) try testing.expect_equal(usize(0), state.live) } all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test { operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal] lefts [3]i64 :: [-2, 2, 3] rights [3]i64 :: [3, 2, -2] operand_types [2]ir.Type :: [.int, .float] for (operand_types) |type| { for (lefts) |left, pair| { right :: rights[pair] for (operations) |op| { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3) module.entry = main left_value := ir.Value{ type = type, data = ir.ValueData{ integer = left } } right_value := ir.Value{ type = type, data = ir.ValueData{ integer = right } } if (type == .float) { left_value.data = ir.ValueData{ real = f64(left) } right_value.data = ir.ValueData{ real = f64(right) } } try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(0), constant = left_value }) try emit(&module, main, ir.Instruction{ op = .constant, type = type, destination = ir.ValueId(1), constant = right_value }) try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) return_value(&module, main, ir.ValueId(2)) expected :: match op { .equal: left == right .not_equal: left != right .less: left < right .less_equal: left <= right .greater: left > right .greater_equal: left >= right else: unreachable } outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(expected, outcome.value.data.boolean) } } } } nan_comparisons_obey_ieee_rules_in_either_operand_position test { operations [6]ir.Op :: [.equal, .not_equal, .less, .less_equal, .greater, .greater_equal] for (operations) |op| { for (usize(0)..usize(3)) |position| { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) main :: try add_function(&module, mem.empty(ir.Type), 0, .bool, 3) module.entry = main try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), constant = ir.Value{ type = .float, data = ir.ValueData{ real = 0 } }, }) try emit(&module, main, ir.Instruction{ op = .divide, type = .float, destination = ir.ValueId(1), left = ir.ValueId(0), right = ir.ValueId(0) }) left ir.ValueId :: if (position == 1) ir.ValueId(0) else ir.ValueId(1) right ir.ValueId :: if (position == 0) ir.ValueId(0) else ir.ValueId(1) try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = left, right = right }) return_value(&module, main, ir.ValueId(2)) outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.bool, outcome.value.type) try testing.expect_equal(op == .not_equal, outcome.value.data.boolean) } } } boolean_equality_and_branching_use_boolean_payloads test { booleans [2]bool :: [false, true] operations [2]ir.Op :: [.equal, .not_equal] for (booleans) |left| { for (booleans) |right| { for (operations) |op| { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) main :: try add_function(&module, mem.empty(ir.Type), 0, .int, 5) module.entry = main yes :: try add_block(&module, main) no :: try add_block(&module, main) try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(0), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = left } } }) try emit(&module, main, ir.Instruction{ op = .constant, type = .bool, destination = ir.ValueId(1), constant = ir.Value{ type = .bool, data = ir.ValueData{ boolean = right } } }) try emit(&module, main, ir.Instruction{ op = op, type = .bool, destination = ir.ValueId(2), left = ir.ValueId(0), right = ir.ValueId(1) }) module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .branch, value = ir.ValueId(2), target = yes, alternative = no } try emit_at(&module, main, yes, integer(3, 42)) module.functions.items[usize(main)].blocks.items[usize(yes)].terminator = ir.Terminator{ value = ir.ValueId(3) } try emit_at(&module, main, no, integer(4, 7)) module.functions.items[usize(main)].blocks.items[usize(no)].terminator = ir.Terminator{ value = ir.ValueId(4) } equal :: left == right expected_true :: if (op == .equal) equal else !equal expected i64 :: if (expected_true) 42 else 7 outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(expected, outcome.value.data.integer) } } } } assign_local_replaces_initialized_values_without_producing_a_result test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 3) module.entry = main try emit(&module, main, integer(0, 1)) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) try emit(&module, main, integer(1, 42)) try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(1) }) try emit(&module, main, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(2), slot = 0 }) return_value(&module, main, ir.ValueId(2)) outcome :: try run(mem.c_allocator, &module, &store) try testing.expect(outcome.trap == null) try testing.expect_equal(ir.Type.int, outcome.value.type) try testing.expect_equal(i64(42), outcome.value.data.integer) try testing.expect_equal(usize(0), diagnostics.count(&store)) } reset_local_invalidates_reads_and_assignments_at_the_instruction_span test { operations [2]ir.Op :: [.load_local, .assign_local] codes [2]diagnostics.Code :: [.read_before_initialization, .write_before_initialization] for (operations) |op, index| { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 2) module.entry = main span :: diagnostics.Span{ start = 21, end = 26 } try emit(&module, main, integer(0, 42)) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) destination ir.ValueId :: if (op == .load_local) ir.ValueId(1) else ir.no_value try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = destination, slot = 0, left = ir.ValueId(0), span = span, }) outcome :: try run(mem.c_allocator, &module, &store) try expect_trap(outcome, &store, codes[index], span) try testing.expect_equal(usize(1), diagnostics.count(&store)) } } premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 1) module.entry = main span :: diagnostics.Span{ start = 14, end = 19 } try emit(&module, main, integer(0, 42)) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0), span = span }) failures := usize(0) succeeded := false for (usize(0)..usize(16)) |limit| { state := AllocationState{ remaining = limit } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } outcome :: run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed } failures += 1 try testing.expect_equal(usize(0), state.live) continue } succeeded = true try expect_trap(outcome, &store, .write_before_initialization, span) try testing.expect_equal(usize(0), state.live) } try testing.expect(succeeded) try testing.expect(failures > 0) } assignment_diagnostic_allocation_failure_releases_runtime_storage test { module := ir.init(mem.c_allocator) defer ir.deinit(&module) types [1]ir.Type :: [.int] main :: try add_function(&module, types[..], 0, .int, 1) module.entry = main try emit(&module, main, integer(0, 42)) try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 0, left = ir.ValueId(0) }) diagnostic_state := AllocationState{ remaining = 0 } diagnostic_allocator :: mem.Allocator{ context = &diagnostic_state, vtable = &limited_vtable } store := diagnostics.init(diagnostic_allocator) defer diagnostics.deinit(&store) state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } _ = run(allocator, &module, &store) catch |error| { match error { .out_of_memory: {} else: return .expectation_failed } try testing.expect_equal(usize(0), state.live) try testing.expect_equal(usize(0), diagnostic_state.live) try testing.expect_equal(usize(0), diagnostics.count(&store)) return } return .expectation_failed }