782 lines
41 KiB
Plaintext
782 lines
41 KiB
Plaintext
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
|
|
}
|