diff --git a/README.md b/README.md index 426d15e..d123d82 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,8 @@ type errors carry source spans and diagnostic identities; invalid operands propa array IR retains recursive element types, counts and mutability. typed aggregate construction, element addresses and load/store instructions use flattened storage reserved per invocation; nested copies and indexed accesses reuse that storage without per-iteration allocation. addresses are internal IR values, not source-language pointers. +IR construction owns derived layout metadata. `ir.add_array` checks shape capacity and computes flattened leaf counts; `ir.add_parameter` and `ir.add_local` create slots with their disjoint array backing. parameters precede other locals. `ir.emit` assigns fresh result IDs by default and maintains temporary counts, array-result segments and address presence. hand-authored non-SSA IR may supply explicit result IDs; repeated array results retain their shape and reuse the same segment. producers do not register backing separately or write derived counts, offsets or flags. construction failures leave logical contents unchanged, and no finalization pass or extra runtime allocation is required. + `interpreter.run` consumes only IR and returns an `Outcome` containing a value or diagnostic trap. returned arrays own their escaped cell storage; release every outcome with `interpreter.deinit` before releasing its module. string values carry a module-local `ir.StringId`, not a slice; resolve bytes with `ir.string_bytes(&module, id)` before releasing the module. decoded bytes and their full backing allocations remain module-owned, without copying strings during calls or returns. array descriptors also borrow the module. heap-backed invocation frames track their current block and resume it after calls or global initialization; loop iterations reuse the invocation's storage without allocating new frames. parameters are initialized on entry and local declarations execute in statement order. premature local reads and assignments trap; assignment evaluates its right-hand side before checking the destination's initialization. globals initialize lazily on their first read, cache their result for that run, and trap on initialization cycles. `int`/`i64` use signed 64-bit storage, `usize` uses unsigned 64-bit storage, both with checked arithmetic; `float`/`f64` use 64-bit IEEE arithmetic and comparisons, including NaN behavior. `usize` supports `+`, `-`, `*` and comparisons, but not unary negation or division. numeric literal overflow and float underflow to zero are diagnosed; representable subnormals are accepted. strings support `\\`, `\"`, `\n`, `\r`, `\t`, `\0` and `\xNN` escapes. recoverable source errors do not stop execution of unaffected code. unused invalid functions, globals and nested bodies retain diagnostic traps; reaching invalid code reports its original diagnostic identity. missing-return traps affect only paths that fall through, not paths that already return or trap. diagnostics include the filename, line and byte column. exit status is `0` for clean execution, `1` for source diagnostics or a runtime trap, and `2` for usage or infrastructure failure. native code generation and compound assignment remain unimplemented. diff --git a/source/interpreter/interpreter.test.hon b/source/interpreter/interpreter.test.hon index 3e22278..10e5c4b 100644 --- a/source/interpreter/interpreter.test.hon +++ b/source/interpreter/interpreter.test.hon @@ -10,10 +10,10 @@ add_function proc( locals []ir.Type, parameter_count usize, result ir.Type, - value_count usize, -) ir.FunctionId ! mem.AllocError { +) ir.FunctionId ! (mem.AllocError | diagnostics.CapacityError) { function := ir.init_function(module.allocator) - errdefer { + transferred := false + errdefer if (!transferred) { arraylist.deinit(&function.parameters) arraylist.deinit(&function.locals) arraylist.deinit(&function.array_storage) @@ -24,22 +24,24 @@ add_function proc( } 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) + transferred = true + for (locals) |type, index| { + if (index < parameter_count) { + _ = try ir.add_parameter(module, id, type, ir.no_array) + } else { + _ = try ir.add_local(module, id, type, ir.no_array) + } + } return id } @hide:file -emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! mem.AllocError { - if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true - try arraylist.append(&module.functions.items[usize(function)].blocks.items[0].instructions, instruction) +emit proc(module @mut ir.Module, function ir.FunctionId, instruction ir.Instruction) void ! (mem.AllocError | diagnostics.CapacityError) { + _ = try ir.emit(module, function, ir.BlockId(0), instruction) } @hide:file @@ -91,15 +93,15 @@ repeated_calls_have_fresh_parameters_locals_and_temporaries test { 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) + bump :: try add_function(&module, types[..], 1, .int) 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, &[], 0, .void, 0) - main :: try add_function(&module, &[], 0, .int, 8) + noop :: try add_function(&module, &[], 0, .void) + main :: try add_function(&module, &[], 0, .int) module.entry = main try emit(&module, main, function_value(0, bump)) try emit(&module, main, integer(1, 19)) @@ -125,8 +127,8 @@ lazy_globals_follow_forward_dependencies_and_reset_between_runs test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - first :: try add_function(&module, &[], 0, .int, 3) - second :: try add_function(&module, &[], 0, .int, 1) + first :: try add_function(&module, &[], 0, .int) + second :: try add_function(&module, &[], 0, .int) 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) }) @@ -135,7 +137,7 @@ lazy_globals_follow_forward_dependencies_and_reset_between_runs test { 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, &[], 0, .int, 3) + main :: try add_function(&module, &[], 0, .int) 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 }) @@ -164,7 +166,7 @@ function_aliases_and_borrowed_string_values_execute test { memcopy!(owned, text) try arraylist.append(&module.strings, ir.StringStorage{ allocation = owned, len = text.len }) transferred = true - target :: try add_function(&module, &[], 0, .string, 1) + target :: try add_function(&module, &[], 0, .string) try emit(&module, target, ir.Instruction{ op = .constant, type = .string, @@ -172,11 +174,11 @@ function_aliases_and_borrowed_string_values_execute test { constant = ir.Value{ type = .string, data = ir.ValueData{ string = ir.StringId(0) } }, }) return_value(&module, target, ir.ValueId(0)) - initializer :: try add_function(&module, &[], 0, .function, 1) + initializer :: try add_function(&module, &[], 0, .function) 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, &[], 0, .string, 2) + main :: try add_function(&module, &[], 0, .string) 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) }) @@ -195,7 +197,7 @@ premature_local_reads_and_global_cycles_report_instruction_spans test { 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) + main :: try add_function(&module, types[..], 0, .int) 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)) @@ -203,8 +205,8 @@ premature_local_reads_and_global_cycles_report_instruction_spans test { defer deinit(&premature) try expect_trap(premature, &store, .read_before_initialization, span) - first :: try add_function(&module, &[], 0, .int, 1) - second :: try add_function(&module, &[], 0, .int, 1) + first :: try add_function(&module, &[], 0, .int) + second :: try add_function(&module, &[], 0, .int) 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 }) @@ -212,7 +214,10 @@ premature_local_reads_and_global_cycles_report_instruction_spans test { 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_main :: try add_function(&module, &[], 0, .int) + module.entry = cycle_main + try emit(&module, cycle_main, ir.Instruction{ op = .load_global, type = .int, destination = ir.ValueId(0), slot = 0 }) + return_value(&module, cycle_main, ir.ValueId(0)) cycle := try run(mem.c_allocator, &module, &store) defer deinit(&cycle) try expect_trap(cycle, &store, .initialization_cycle, cycle_span) @@ -232,9 +237,9 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test { defer deinit(&absent) 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, &[], 0, .invalid, 0) + bad :: try add_function(&module, &[], 0, .invalid) module.functions.items[usize(bad)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = invalid_id } - main :: try add_function(&module, &[], 0, .int, 2) + main :: try add_function(&module, &[], 0, .int) module.entry = main try emit(&module, main, integer(0, 42)) return_value(&module, main, ir.ValueId(0)) @@ -242,8 +247,11 @@ existing_trap_identity_entry_failures_and_unused_errors_survive test { defer deinit(&success) 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) }) + trap_main :: try add_function(&module, &[], 0, .int) + module.entry = trap_main + try emit(&module, trap_main, function_value(0, bad)) + try emit(&module, trap_main, ir.Instruction{ op = .call, type = .invalid, destination = ir.ValueId(1), left = ir.ValueId(0) }) + return_value(&module, trap_main, ir.ValueId(0)) trapped := try run(mem.c_allocator, &module, &store) defer deinit(&trapped) try testing.expect_equal(invalid_id, trapped.trap?) @@ -286,7 +294,7 @@ integer_arithmetic_checks_boundaries_before_host_operations test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) span :: diagnostics.Span{ start = 8, end = 16 } - main :: try add_function(&module, &[], 0, .int, 3) + main :: try add_function(&module, &[], 0, .int) module.entry = main try emit(&module, main, integer(0, case.left)) try emit(&module, main, integer(1, case.right)) @@ -309,7 +317,7 @@ integer_negation_traps_only_at_minimum test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) span :: diagnostics.Span{ start = 21, end = 27 } - main :: try add_function(&module, &[], 0, .int, 2) + main :: try add_function(&module, &[], 0, .int) 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 }) @@ -329,7 +337,7 @@ float_arithmetic_and_ieee_zero_division_execute test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .float, 7) + main :: try add_function(&module, &[], 0, .float) 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 } } }) @@ -413,7 +421,7 @@ recursive_invocations_use_heap_frames_and_free_on_allocation_failure test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .void, 2) + main :: try add_function(&module, &[], 0, .void) 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 } @@ -440,13 +448,13 @@ frames_and_globals_are_freed_at_every_allocation_boundary_and_trap test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] - initializer :: try add_function(&module, types[..], 0, .int, 1) + initializer :: try add_function(&module, types[..], 0, .int) 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) + main :: try add_function(&module, types[..], 0, .int) module.entry = main work :: try add_block(&module, main) module.functions.items[usize(main)].blocks.items[0].terminator = ir.Terminator{ kind = .jump, target = work } @@ -490,7 +498,7 @@ runtime_diagnostic_allocation_failure_is_infrastructure_error_and_frees_frames t module := ir.init(mem.c_allocator) defer ir.deinit(&module) types [1]ir.Type :: [.int] - main :: try add_function(&module, types[..], 0, .int, 1) + main :: try add_function(&module, types[..], 0, .int) 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)) @@ -523,9 +531,8 @@ add_block proc(module @mut ir.Module, function ir.FunctionId) ir.BlockId ! mem.A } @hide:file -emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! mem.AllocError { - if (instruction.type == .address) module.functions.items[usize(function)].has_addresses = true - try arraylist.append(&module.functions.items[usize(function)].blocks.items[usize(block)].instructions, instruction) +emit_at proc(module @mut ir.Module, function ir.FunctionId, block ir.BlockId, instruction ir.Instruction) void ! (mem.AllocError | diagnostics.CapacityError) { + _ = try ir.emit(module, function, block, instruction) } branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { @@ -534,7 +541,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] - count :: try add_function(&module, types[..], 1, .int, 9) + count :: try add_function(&module, types[..], 1, .int) 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 }) @@ -552,7 +559,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { 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, &[], 0, .int, 3) + initializer :: try add_function(&module, &[], 0, .int) 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 }) @@ -566,7 +573,7 @@ branching_recursive_calls_and_lazy_initializers_resume_the_current_block test { 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, &[], 0, .int, 5) + main :: try add_function(&module, &[], 0, .int) main_entry :: try add_block(&module, main) main_exit :: try add_block(&module, main) module.entry = main @@ -611,7 +618,7 @@ all_numeric_comparisons_dispatch_on_operand_type_not_bool_result_type test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .bool, 3) + main :: try add_function(&module, &[], 0, .bool) 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 } } @@ -650,7 +657,7 @@ nan_comparisons_obey_ieee_rules_in_either_operand_position test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .bool, 3) + main :: try add_function(&module, &[], 0, .bool) module.entry = main try emit(&module, main, ir.Instruction{ op = .constant, type = .float, destination = ir.ValueId(0), @@ -680,7 +687,7 @@ boolean_equality_and_branching_use_boolean_payloads test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .int, 5) + main :: try add_function(&module, &[], 0, .int) module.entry = main yes :: try add_block(&module, main) no :: try add_block(&module, main) @@ -710,7 +717,7 @@ assign_local_replaces_initialized_values_without_producing_a_result test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] - main :: try add_function(&module, types[..], 0, .int, 3) + main :: try add_function(&module, types[..], 0, .int) 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) }) @@ -735,7 +742,7 @@ reset_local_invalidates_reads_and_assignments_at_the_instruction_span test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] - main :: try add_function(&module, types[..], 0, .int, 2) + main :: try add_function(&module, types[..], 0, .int) module.entry = main span :: diagnostics.Span{ start = 21, end = 26 } try emit(&module, main, integer(0, 42)) @@ -758,7 +765,7 @@ premature_assignment_and_reset_traps_release_frames_at_all_allocation_boundaries store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [1]ir.Type :: [.int] - main :: try add_function(&module, types[..], 0, .int, 1) + main :: try add_function(&module, types[..], 0, .int) module.entry = main span :: diagnostics.Span{ start = 14, end = 19 } try emit(&module, main, integer(0, 42)) @@ -793,7 +800,7 @@ 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) + main :: try add_function(&module, types[..], 0, .int) 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) }) @@ -825,7 +832,7 @@ logical_not_inverts_boolean_payload_and_composes_in_typed_ir test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .bool, 3) + main :: try add_function(&module, &[], 0, .bool) module.entry = main try emit(&module, main, ir.Instruction{ op = .constant, @@ -859,27 +866,12 @@ unsigned proc(destination u32, value u64) ir.Instruction { } @hide:file -add_array proc(module @mut ir.Module, element ir.Type, child ir.ArrayId, len usize) ir.ArrayId ! mem.AllocError { - width :: if (element == .array) module.arrays.items[usize(child)].cells else 1 - id :: ir.ArrayId(module.arrays.items.len) - try arraylist.append(&module.arrays, ir.ArrayType{ - element = element, element_array = child, len = len, cells = len * width, mutable = true, - }) - return id +add_array proc(module @mut ir.Module, element ir.Type, child ir.ArrayId, len usize) ir.ArrayId ! (mem.AllocError | diagnostics.CapacityError) { + return try ir.add_array(module, element, child, len, true) } @hide:file -array_storage proc(module @mut ir.Module, function ir.FunctionId, index u32, shape ir.ArrayId, local bool) void ! mem.AllocError { - body :: &module.functions.items[usize(function)] - try arraylist.append(&body.array_storage, ir.ArrayStorage{ - index = index, array_type = shape, offset = body.aggregate_cells, local = local, - }) - body.aggregate_cells += module.arrays.items[usize(shape)].cells -} - -@hide:file -literal proc(module @mut ir.Module, function ir.FunctionId, destination u32, shape ir.ArrayId, elements []u32) void ! mem.AllocError { - try array_storage(module, function, destination, shape, false) +literal proc(module @mut ir.Module, function ir.FunctionId, destination u32, shape ir.ArrayId, elements []u32) void ! (mem.AllocError | diagnostics.CapacityError) { block :: &module.functions.items[usize(function)].blocks.items[0] start :: block.arguments.items.len for (elements) |element| try arraylist.append(&block.arguments, ir.ValueId(element)) @@ -896,12 +888,12 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test { defer diagnostics.deinit(&store) row :: try add_array(&module, .int, ir.no_array, 2) matrix :: try add_array(&module, .array, row, 2) - types [2]ir.Type :: [.array, .array] - main :: try add_function(&module, types[..], 0, .array, 15) + main :: try add_function(&module, &[], 0, .array) + _ = try ir.add_local(&module, main, .array, matrix) + _ = try ir.add_local(&module, main, .array, matrix) module.entry = main module.functions.items[usize(main)].result_array_type = matrix - try array_storage(&module, main, 0, matrix, true) - try array_storage(&module, main, 1, matrix, true) + try emit(&module, main, integer(0, 1)) try emit(&module, main, integer(1, 2)) pair [2]u32 :: [0, 1] @@ -909,14 +901,14 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test { rows [2]u32 :: [2, 2] try literal(&module, main, 3, matrix, rows[..]) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(3) }) - try array_storage(&module, main, 4, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(4), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 1, left = ir.ValueId(4) }) try emit(&module, main, unsigned(5, 0)) try emit(&module, main, unsigned(6, 1)) try emit(&module, main, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = matrix, destination = ir.ValueId(7), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(5) }) - try array_storage(&module, main, 9, row, false) + try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = row, destination = ir.ValueId(9), left = ir.ValueId(8) }) try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(10), left = ir.ValueId(8), right = ir.ValueId(6) }) try emit(&module, main, integer(11, 9)) @@ -926,12 +918,12 @@ nested_array_places_and_rvalues_copy_only_their_typed_leaf_segments test { try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(11) }) try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .array, array_type = row, destination = ir.ValueId(8), left = ir.ValueId(7), right = ir.ValueId(6) }) try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(8), right = ir.ValueId(9) }) - try array_storage(&module, main, 12, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(12), slot = 0 }) try emit(&module, main, ir.Instruction{ op = .assign_local, slot = 1, left = ir.ValueId(12) }) try emit(&module, main, integer(13, 7)) try emit(&module, main, ir.Instruction{ op = .store_address, left = ir.ValueId(10), right = ir.ValueId(13) }) - try array_storage(&module, main, 14, matrix, false) + try emit(&module, main, ir.Instruction{ op = .load_local, type = .array, array_type = matrix, destination = ir.ValueId(14), slot = 1 }) return_value(&module, main, ir.ValueId(14)) outcome := try run(mem.c_allocator, &module, &store) @@ -949,13 +941,11 @@ array_arguments_and_returns_are_private_values_before_frame_release test { defer diagnostics.deinit(&store) shape :: try add_array(&module, .int, ir.no_array, 1) pair_shape :: try add_array(&module, .int, ir.no_array, 2) - types [2]ir.Type :: [.array, .array] - callee :: try add_function(&module, types[..], 1, .array, 6) + callee :: try add_function(&module, &[], 0, .array) + _ = try ir.add_parameter(&module, callee, .array, shape) + _ = try ir.add_local(&module, callee, .array, shape) module.functions.items[usize(callee)].result_array_type = shape - try array_storage(&module, callee, 0, shape, true) - try array_storage(&module, callee, 1, shape, true) - try array_storage(&module, callee, 0, shape, false) - try array_storage(&module, callee, 5, shape, false) + try emit(&module, callee, ir.Instruction{ op = .load_local, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 }) try emit(&module, callee, ir.Instruction{ op = .store_local, left = ir.ValueId(0), slot = 1 }) try emit(&module, callee, ir.Instruction{ op = .address_local, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(1), slot = 1 }) @@ -970,14 +960,14 @@ array_arguments_and_returns_are_private_values_before_frame_release test { try emit(&module, callee, integer(4, 7)) try emit(&module, callee, ir.Instruction{ op = .store_address, left = ir.ValueId(3), right = ir.ValueId(4) }) return_value(&module, callee, ir.ValueId(5)) - main :: try add_function(&module, &[], 0, .array, 8) + main :: try add_function(&module, &[], 0, .array) module.entry = main module.functions.items[usize(main)].result_array_type = pair_shape try emit(&module, main, integer(0, 42)) element [1]u32 :: [0] try literal(&module, main, 1, shape, element[..]) try emit(&module, main, function_value(2, callee)) - try array_storage(&module, main, 3, shape, false) + try arraylist.append(&module.functions.items[usize(main)].blocks.items[0].arguments, ir.ValueId(1)) try emit(&module, main, ir.Instruction{ op = .call, type = .array, array_type = shape, destination = ir.ValueId(3), left = ir.ValueId(2), arguments_start = 1, arguments_count = 1 }) try emit(&module, main, unsigned(4, 0)) @@ -999,11 +989,11 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) shape :: try add_array(&module, .int, ir.no_array, 1) - first :: try add_function(&module, &[], 0, .array, 1) - second :: try add_function(&module, &[], 0, .array, 2) + first :: try add_function(&module, &[], 0, .array) + second :: try add_function(&module, &[], 0, .array) module.functions.items[usize(first)].result_array_type = shape module.functions.items[usize(second)].result_array_type = shape - try array_storage(&module, first, 0, shape, false) + try emit(&module, first, ir.Instruction{ op = .load_global, type = .array, array_type = shape, destination = ir.ValueId(0), slot = 1 }) return_value(&module, first, ir.ValueId(0)) try emit(&module, second, integer(0, 42)) @@ -1017,11 +1007,11 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t span = diagnostics.Span{ start = 0, end = 0 }, }) } - main :: try add_function(&module, &[], 0, .array, 2) + main :: try add_function(&module, &[], 0, .array) module.entry = main module.functions.items[usize(main)].result_array_type = shape try emit(&module, main, ir.Instruction{ op = .address_global, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(0), slot = 0 }) - try array_storage(&module, main, 1, shape, false) + try emit(&module, main, ir.Instruction{ op = .load_address, type = .array, array_type = shape, destination = ir.ValueId(1), left = ir.ValueId(0) }) return_value(&module, main, ir.ValueId(1)) succeeded := false @@ -1049,11 +1039,18 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t try testing.expect(failures > 0) # restarting a run clears global readiness, including address-triggered initialization cycles. cycle_span :: diagnostics.Span{ start = 51, end = 59 } - module.functions.items[usize(second)].has_addresses = true - module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = ir.Instruction{ + cycle_initializer :: try add_function(&module, &[], 0, .array) + module.functions.items[usize(cycle_initializer)].result_array_type = shape + try emit(&module, cycle_initializer, ir.Instruction{ op = .address_global, type = .address, pointee = .array, array_type = shape, destination = ir.ValueId(0), slot = 0, span = cycle_span, - } + }) + try emit(&module, cycle_initializer, ir.Instruction{ + op = .load_address, type = .array, array_type = shape, + destination = ir.ValueId(1), left = ir.ValueId(0), + }) + return_value(&module, cycle_initializer, ir.ValueId(1)) + module.globals.items[1].initializer = cycle_initializer state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } cycle := try run(allocator, &module, &store) @@ -1063,7 +1060,7 @@ lazy_aggregate_globals_escape_with_owned_results_and_release_at_every_boundary t try testing.expect_equal(usize(0), state.live) trap_id :: try diagnostics.add(&store, .invalid_escape, cycle_span) module.functions.items[usize(second)].blocks.items[0].terminator = ir.Terminator{ kind = .trap, diagnostic = trap_id } - module.functions.items[usize(second)].blocks.items[0].instructions.items[0] = integer(0, 42) + module.globals.items[1].initializer = second trapped := try run(allocator, &module, &store) defer deinit(&trapped) try testing.expect_equal(trap_id, trapped.trap?) @@ -1083,10 +1080,10 @@ array_bounds_check_zero_lengths_and_the_full_unsigned_word_before_addressing tes store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) shape :: try add_array(&module, .int, ir.no_array, length) - types [1]ir.Type :: [.array] - main :: try add_function(&module, types[..], 0, .int, 5) + main :: try add_function(&module, &[], 0, .int) + _ = try ir.add_local(&module, main, .array, shape) module.entry = main - try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 42)) operands [1]u32 :: [0] try literal(&module, main, 1, shape, operands[..length]) @@ -1129,7 +1126,7 @@ zero_leaf_nested_arrays_return_a_shape_without_an_owned_allocation test { defer diagnostics.deinit(&store) empty :: try add_array(&module, .bool, ir.no_array, 0) rows :: try add_array(&module, .array, empty, 3) - main :: try add_function(&module, &[], 0, .array, 4) + main :: try add_function(&module, &[], 0, .array) module.entry = main module.functions.items[usize(main)].result_array_type = rows try literal(&module, main, 0, empty, &[]) @@ -1148,7 +1145,7 @@ zero_leaf_nested_arrays_return_a_shape_without_an_owned_allocation test { deinit(&outcome) try testing.expect_equal(usize(0), state.live) try emit(&module, main, unsigned(2, 2)) - try array_storage(&module, main, 3, empty, false) + try emit(&module, main, ir.Instruction{ op = .index, type = .array, array_type = empty, destination = ir.ValueId(3), left = ir.ValueId(1), right = ir.ValueId(2) }) return_value(&module, main, ir.ValueId(3)) module.functions.items[usize(main)].result_array_type = empty @@ -1189,7 +1186,7 @@ unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .usize, 3) + main :: try add_function(&module, &[], 0, .usize) module.entry = main span :: diagnostics.Span{ start = 7, end = 12 } try emit(&module, main, unsigned(0, case.left)) @@ -1211,7 +1208,7 @@ unsigned_arithmetic_and_comparisons_preserve_all_sixty_four_bits test { defer ir.deinit(&module) store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - main :: try add_function(&module, &[], 0, .bool, 3) + main :: try add_function(&module, &[], 0, .bool) module.entry = main try emit(&module, main, unsigned(0, maxval!(u64))) try emit(&module, main, unsigned(1, 1)) @@ -1244,11 +1241,11 @@ all_scalar_leaf_families_survive_aggregate_return_and_function_extraction test { } store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) - target :: try add_function(&module, &[], 0, .int, 1) + target :: try add_function(&module, &[], 0, .int) try emit(&module, target, integer(0, 42)) return_value(&module, target, ir.ValueId(0)) shape :: try add_array(&module, scalar.type, ir.no_array, 1) - main :: try add_function(&module, &[], 0, .array, 5) + main :: try add_function(&module, &[], 0, .array) module.entry = main module.functions.items[usize(main)].result_array_type = shape try emit(&module, main, ir.Instruction{ op = .constant, type = scalar.type, destination = ir.ValueId(0), constant = scalar }) @@ -1288,10 +1285,10 @@ array_address_initialization_flags_survive_reset_and_diagnostic_allocation_failu module := ir.init(mem.c_allocator) defer ir.deinit(&module) shape :: try add_array(&module, .int, ir.no_array, 1) - types [1]ir.Type :: [.array] - main :: try add_function(&module, types[..], 0, .int, 5) + main :: try add_function(&module, &[], 0, .int) + _ = try ir.add_local(&module, main, .array, shape) module.entry = main - try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 42)) operand [1]u32 :: [0] try literal(&module, main, 1, shape, operand[..]) @@ -1301,7 +1298,8 @@ array_address_initialization_flags_survive_reset_and_diagnostic_allocation_failu try emit(&module, main, ir.Instruction{ op = .index_address, type = .address, pointee = .int, destination = ir.ValueId(4), left = ir.ValueId(2), right = ir.ValueId(3) }) try emit(&module, main, ir.Instruction{ op = .reset_local, slot = 0 }) span :: diagnostics.Span{ start = 23, end = 37 } - try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = ir.ValueId(0), left = ir.ValueId(4), right = ir.ValueId(0), span = span }) + destination :: if (op == .load_address) ir.ValueId(0) else ir.no_value + try emit(&module, main, ir.Instruction{ op = op, type = .int, destination = destination, left = ir.ValueId(4), right = ir.ValueId(0), span = span }) for (usize(0)..usize(2)) |attempt| { state := AllocationState{ remaining = 64 } allocator :: mem.Allocator{ context = &state, vtable = &limited_vtable } @@ -1332,10 +1330,11 @@ aggregate_loop_places_reuse_storage_without_allocation_per_iteration test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) shape :: try add_array(&module, .int, ir.no_array, 1) - types [2]ir.Type :: [.array, .usize] - main :: try add_function(&module, types[..], 0, .int, 14) + main :: try add_function(&module, &[], 0, .int) + _ = try ir.add_local(&module, main, .array, shape) + _ = try ir.add_local(&module, main, .usize, ir.no_array) module.entry = main - try array_storage(&module, main, 0, shape, true) + try emit(&module, main, integer(0, 0)) operand [1]u32 :: [0] try literal(&module, main, 1, shape, operand[..]) @@ -1389,9 +1388,10 @@ recursive_array_backings_and_addresses_remain_fresh_across_stack_growth test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) shape :: try add_array(&module, .int, ir.no_array, 1) - types [2]ir.Type :: [.int, .array] - recursive :: try add_function(&module, types[..], 1, .int, 12) - try array_storage(&module, recursive, 1, shape, true) + recursive :: try add_function(&module, &[], 0, .int) + _ = try ir.add_parameter(&module, recursive, .int, ir.no_array) + _ = try ir.add_local(&module, recursive, .array, shape) + try emit(&module, recursive, ir.Instruction{ op = .load_local, type = .int, destination = ir.ValueId(0), slot = 0 }) operand [1]u32 :: [0] try literal(&module, recursive, 1, shape, operand[..]) @@ -1413,7 +1413,7 @@ recursive_array_backings_and_addresses_remain_fresh_across_stack_growth test { try emit_at(&module, recursive, recurse, ir.Instruction{ op = .load_address, type = .int, destination = ir.ValueId(10), left = ir.ValueId(4) }) try emit_at(&module, recursive, recurse, ir.Instruction{ op = .add, type = .int, destination = ir.ValueId(11), left = ir.ValueId(9), right = ir.ValueId(10) }) module.functions.items[usize(recursive)].blocks.items[usize(recurse)].terminator = ir.Terminator{ value = ir.ValueId(11) } - main :: try add_function(&module, &[], 0, .int, 3) + main :: try add_function(&module, &[], 0, .int) module.entry = main try emit(&module, main, function_value(0, recursive)) try emit(&module, main, integer(1, 64)) @@ -1432,7 +1432,7 @@ inclusive_unsigned_range_reaches_the_maximum_without_incrementing_past_it test { store := diagnostics.init(mem.c_allocator) defer diagnostics.deinit(&store) types [2]ir.Type :: [.usize, .usize] - main :: try add_function(&module, types[..], 0, .usize, 9) + main :: try add_function(&module, types[..], 0, .usize) module.entry = main try emit(&module, main, unsigned(0, maxval!(u64) - 1)) try emit(&module, main, ir.Instruction{ op = .store_local, slot = 0, left = ir.ValueId(0) }) diff --git a/source/ir/ir.hon b/source/ir/ir.hon index 4be8a25..2890197 100644 --- a/source/ir/ir.hon +++ b/source/ir/ir.hon @@ -1,5 +1,6 @@ import "@std/mem" import "@std/arraylist" +import "@std/debug" import "@source/diagnostics" FunctionId :: distinct u32 @@ -41,7 +42,7 @@ Value :: struct { data ValueData = ValueData{ integer = 0 } } -#! fixed recursive shape; `cells` counts flattened scalar leaves, including zero-sized children. +#! fixed recursive shape; add_array derives `cells`, the flattened scalar-leaf count. ArrayType :: struct { element Type element_array ArrayId = no_array @@ -124,6 +125,8 @@ Block :: struct { terminator Terminator = Terminator{} } +#! construction derives storage segments, aggregate totals, value counts, and address presence. +#! producers use add_local, add_parameter, and emit; derived metadata is read-only to callers. Function :: struct { parameters arraylist.ArrayList(Type) locals arraylist.ArrayList(Type) @@ -189,6 +192,125 @@ init_block proc(allocator mem.Allocator) Block { } } +#! adds a fixed shape and derives its flattened storage size; child shapes must already exist. +#! scalar elements use no_array; zero-leaf children retain their logical lengths. +add_array proc( + module @mut Module, + element Type, + element_array ArrayId, + len usize, + mutable bool, +) ArrayId ! (mem.AllocError | diagnostics.CapacityError) { + debug.assert((element == .array) == (element_array != no_array)) + width :: if (element == .array) module.arrays.items[usize(element_array)].cells else 1 + if (width != 0 and len > divtrunc!(maxval!(usize), width)) return .storage_limit_reached + cells :: len * width + if (cells > divtrunc!(maxval!(usize), sizeof!(Value))) return .storage_limit_reached + if (module.arrays.items.len >= usize(no_array)) return .storage_limit_reached + id :: ArrayId(module.arrays.items.len) + try arraylist.append(&module.arrays, ArrayType{ + element = element, + element_array = element_array, + len = len, + cells = cells, + mutable = mutable, + }) + return id +} + +@hide:file +prepare_storage proc(module @Module, function @mut Function, index u32, shape ArrayId, local bool) void ! (mem.AllocError | diagnostics.CapacityError) { + cells :: module.arrays.items[usize(shape)].cells + if (cells > divtrunc!(maxval!(usize), sizeof!(Value)) - function.aggregate_cells) return .storage_limit_reached + try arraylist.append(&function.array_storage, ArrayStorage{ + index = index, + array_type = shape, + offset = function.aggregate_cells, + local = local, + }) + function.aggregate_cells += cells +} + +#! creates a local and its disjoint by-value backing together; scalar locals use no_array. +add_local proc( + module @mut Module, + function FunctionId, + type Type, + shape ArrayId, +) LocalId ! (mem.AllocError | diagnostics.CapacityError) { + debug.assert((type == .array) == (shape != no_array)) + body :: &module.functions.items[usize(function)] + if (body.locals.items.len >= usize(maxval!(LocalId))) return .storage_limit_reached + id :: LocalId(body.locals.items.len) + try arraylist.append(&body.locals, type) + errdefer _ = arraylist.pop(&body.locals) + if (type == .array) try prepare_storage(module, body, u32(id), shape, true) + return id +} + +#! creates a parameter and its local backing; all parameters precede non-parameter locals. +add_parameter proc( + module @mut Module, + function FunctionId, + type Type, + shape ArrayId, +) LocalId ! (mem.AllocError | diagnostics.CapacityError) { + body :: &module.functions.items[usize(function)] + debug.assert(body.parameters.items.len == body.locals.items.len) + try arraylist.append(&body.parameters, type) + errdefer _ = arraylist.pop(&body.parameters) + return try add_local(module, function, type, shape) +} + +#! appends an instruction and derives its result count, array backing, and address presence. +#! no_value requests a fresh result; explicit result IDs support non-SSA definitions across blocks. +#! repeated array results must retain their shape and reuse their existing segment. +#! failures leave instruction and storage lengths, totals, counts, and flags unchanged. +emit proc( + module @mut Module, + function FunctionId, + block BlockId, + instruction Instruction, +) ValueId ! (mem.AllocError | diagnostics.CapacityError) { + body :: &module.functions.items[usize(function)] + target :: &body.blocks.items[usize(block)] + operation := instruction + produces_value :: ( + operation.op != .store_local + and operation.op != .assign_local + and operation.op != .reset_local + and operation.op != .store_address + ) + count := body.value_count + if (produces_value) { + if (operation.destination == no_value) { + if (count >= usize(no_value)) return .storage_limit_reached + operation.destination = ValueId(count) + } + if (usize(operation.destination) >= count) count = usize(operation.destination) + 1 + } else { + debug.assert(operation.destination == no_value) + } + try arraylist.append(&target.instructions, operation) + errdefer _ = arraylist.pop(&target.instructions) + if (produces_value and operation.type == .array) { + debug.assert(operation.array_type != no_array) + registered := false + if (usize(operation.destination) < body.value_count) { + for (body.array_storage.items) |storage| { + if (storage.local or storage.index != u32(operation.destination)) continue + debug.assert(storage.array_type == operation.array_type) + registered = true + break + } + } + if (!registered) try prepare_storage(module, body, u32(operation.destination), operation.array_type, false) + } + body.value_count = count + if (operation.type == .address) body.has_addresses = true + return operation.destination +} + #! resolves a module-local ID to bytes borrowed until module deinit. string_bytes proc(module @Module, id StringId) []u8 { string :: module.strings.items[usize(id)] diff --git a/source/ir/ir.test.hon b/source/ir/ir.test.hon new file mode 100644 index 0000000..929886c --- /dev/null +++ b/source/ir/ir.test.hon @@ -0,0 +1,61 @@ +import "@std/mem" +import "@std/arraylist" +import "@std/testing" + +shape_and_segment_limits_preserve_zero_leaf_arrays_and_allow_retry test { + module := init(mem.c_allocator) + defer deinit(&module) + limit :: divtrunc!(maxval!(usize), sizeof!(Value)) + rejected := false + _ = add_array(&module, .int, no_array, limit + 1, false) catch |error| { + rejected = error == .storage_limit_reached + yield no_array + } + try testing.expect(rejected) + pair :: try add_array(&module, .int, no_array, 2, false) + rejected = false + _ = add_array(&module, .array, pair, maxval!(usize), false) catch |error| { + rejected = error == .storage_limit_reached + yield no_array + } + try testing.expect(rejected) + full :: try add_array(&module, .int, no_array, limit - 2, false) + empty :: try add_array(&module, .bool, no_array, 0, false) + zero_rows :: try add_array(&module, .array, empty, maxval!(usize), false) + try arraylist.append(&module.functions, init_function(module.allocator)) + function :: FunctionId(0) + try arraylist.append(&module.functions.items[0].blocks, init_block(module.allocator)) + try testing.expect_equal(LocalId(0), try add_parameter(&module, function, .array, full)) + element :: try emit(&module, function, BlockId(0), Instruction{ + op = .constant, type = .int, constant = Value{ type = .int, data = ValueData{ integer = 42 } }, + }) + try arraylist.append(&module.functions.items[0].blocks.items[0].arguments, element) + try arraylist.append(&module.functions.items[0].blocks.items[0].arguments, element) + result :: try emit(&module, function, BlockId(0), Instruction{ + op = .array_literal, type = .array, array_type = pair, arguments_count = 2, + }) + # redefining an array result reuses backing even when no further cells fit. + _ = try emit(&module, function, BlockId(0), Instruction{ + op = .array_literal, type = .array, array_type = pair, arguments_count = 2, destination = result, + }) + rejected = false + _ = add_parameter(&module, function, .array, pair) catch |error| { + rejected = error == .storage_limit_reached + yield LocalId(0) + } + try testing.expect(rejected) + # a rejected parameter consumes neither a parameter/local identity nor aggregate storage. + try testing.expect_equal(LocalId(1), try add_parameter(&module, function, .array, zero_rows)) + rejected = false + _ = emit(&module, function, BlockId(0), Instruction{ + op = .array_literal, type = .array, array_type = pair, arguments_count = 2, + }) catch |error| { + rejected = error == .storage_limit_reached + yield no_value + } + try testing.expect(rejected) + # a rejected result consumes no temporary; zero-leaf shapes still fit a full segment layout. + try testing.expect_equal(ValueId(2), try emit(&module, function, BlockId(0), Instruction{ + op = .load_local, type = .array, array_type = zero_rows, slot = 1, + })) +} diff --git a/source/lower/lower.hon b/source/lower/lower.hon index ea19a22..80b6efa 100644 --- a/source/lower/lower.hon +++ b/source/lower/lower.hon @@ -129,41 +129,18 @@ prepare_array proc(state @mut State, id typecheck.ArrayId) ir.ArrayId ! (mem.All if (state.arrays[usize(id)] != ir.no_array) return state.arrays[usize(id)] descriptor :: typecheck.array_type(state.checked, id) child := ir.no_array - width := usize(1) - if (descriptor.element.kind == .array) { - child = try prepare_array(state, descriptor.element.array_type) - width = state.module.arrays.items[usize(child)].cells - } - if (width != 0 and descriptor.len > divtrunc!(maxval!(usize), width)) return .storage_limit_reached - if (descriptor.len * width > divtrunc!(maxval!(usize), sizeof!(ir.Value))) return .storage_limit_reached - if (state.module.arrays.items.len >= usize(ir.no_array)) return .storage_limit_reached - result :: ir.ArrayId(state.module.arrays.items.len) - try arraylist.append(&state.module.arrays, ir.ArrayType{ - element = lower_type(descriptor.element), - element_array = child, - len = descriptor.len, - cells = descriptor.len * width, - mutable = descriptor.mutable, - }) + if (descriptor.element.kind == .array) child = try prepare_array(state, descriptor.element.array_type) + result :: try ir.add_array( + state.module, + lower_type(descriptor.element), + child, + descriptor.len, + descriptor.mutable, + ) state.arrays[usize(id)] = result return result } -@hide:file -prepare_storage proc(state @mut State, index u32, shape ir.ArrayId, local bool) void ! (mem.AllocError | diagnostics.CapacityError) { - if (shape == ir.no_array) return - function := &state.module.functions.items[state.function] - cells :: state.module.arrays.items[usize(shape)].cells - if (cells > divtrunc!(maxval!(usize), sizeof!(ir.Value)) - function.aggregate_cells) return .storage_limit_reached - try arraylist.append(&function.array_storage, ir.ArrayStorage{ - index = index, - array_type = shape, - offset = function.aggregate_cells, - local = local, - }) - function.aggregate_cells += cells -} - @hide:file add_function proc(module @mut ir.Module, span diagnostics.Span) ir.FunctionId ! mem.AllocError { id :: ir.FunctionId(module.functions.items.len) @@ -239,17 +216,7 @@ finish proc(state @mut State, value ir.ValueId, span diagnostics.Span) void { @hide:file emit proc(state @mut State, instruction ir.Instruction) ir.ValueId ! (mem.AllocError | diagnostics.CapacityError) { - function := &state.module.functions.items[state.function] - operation := instruction - if (operation.op != .store_local and operation.op != .assign_local and operation.op != .reset_local and operation.op != .store_address) { - if (function.value_count >= usize(ir.no_value)) return .storage_limit_reached - operation.destination = ir.ValueId(function.value_count) - function.value_count += 1 - if (operation.type == .array) try prepare_storage(state, u32(operation.destination), operation.array_type, false) - } - if (operation.type == .address) function.has_addresses = true - try arraylist.append(&function.blocks.items[usize(state.block)].instructions, operation) - return operation.destination + return try ir.emit(state.module, ir.FunctionId(state.function), state.block, instruction) } @hide:file @@ -280,16 +247,18 @@ lower_function proc(state @mut State, id ast.NodeId) void ! (mem.AllocError | di state.function = usize(state.storage[usize(id)].index) state.block = ir.BlockId(0) state.terminated = false - function := &state.module.functions.items[state.function] - function.result = lower_type(typecheck.type_of(state.checked, declaration.return_type)) - function.result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type)) + state.module.functions.items[state.function].result = lower_type(typecheck.type_of(state.checked, declaration.return_type)) + state.module.functions.items[state.function].result_array_type = try array_shape(state, typecheck.type_of(state.checked, declaration.return_type)) for (0..ast.function_param_count(tree, id)) |i| { parameter :: ast.function_param(tree, id, i) - type :: lower_type(typecheck.type_of(state.checked, parameter)) - state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(function.locals.items.len) } - try arraylist.append(&function.parameters, type) - try arraylist.append(&function.locals, type) - try prepare_storage(state, state.storage[usize(parameter)].index, try array_shape(state, typecheck.type_of(state.checked, parameter)), true) + type :: typecheck.type_of(state.checked, parameter) + local :: try ir.add_parameter( + state.module, + ir.FunctionId(state.function), + lower_type(type), + try array_shape(state, type), + ) + state.storage[usize(parameter)] = Storage{ kind = .local, index = u32(local) } } try predeclare_locals(state, declaration.body) # signature errors are entry traps; body errors are reached in statement order. @@ -313,10 +282,14 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro statement :: ast.block_statement(state.tree, block, i) match ast.stmt_kind(state.tree, statement) { .declaration: { - function := &state.module.functions.items[state.function] - state.storage[usize(statement)] = Storage{ kind = .local, index = u32(function.locals.items.len) } - try arraylist.append(&function.locals, lower_type(typecheck.type_of(state.checked, statement))) - try prepare_storage(state, state.storage[usize(statement)].index, try array_shape(state, typecheck.type_of(state.checked, statement)), true) + type :: typecheck.type_of(state.checked, statement) + local :: try ir.add_local( + state.module, + ir.FunctionId(state.function), + lower_type(type), + try array_shape(state, type), + ) + state.storage[usize(statement)] = Storage{ kind = .local, index = u32(local) } } .if_stmt: { conditional :: ast.if_stmt(state.tree, statement) @@ -326,16 +299,14 @@ predeclare_locals proc(state @mut State, block ast.NodeId) void ! (mem.AllocErro .while_stmt: try predeclare_locals(state, ast.while_stmt(state.tree, statement).body) .for_stmt: { loop :: ast.for_stmt(state.tree, statement) - function := &state.module.functions.items[state.function] - slot :: u32(function.locals.items.len) family :: typecheck.type_of(state.checked, loop.iterable).range_element type ir.Type :: if (family == .usize) .usize else .int - try arraylist.append(&function.locals, type) - state.storage[usize(statement)] = Storage{ kind = .local, index = slot } + slot :: try ir.add_local(state.module, ir.FunctionId(state.function), type, ir.no_array) + state.storage[usize(statement)] = Storage{ kind = .local, index = u32(slot) } if (loop.capture) |capture| state.storage[usize(capture)] = state.storage[usize(statement)] if (loop.index) |index| { - state.storage[usize(index)] = Storage{ kind = .local, index = u32(function.locals.items.len) } - try arraylist.append(&function.locals, ir.Type.int) + index_slot :: try ir.add_local(state.module, ir.FunctionId(state.function), .int, ir.no_array) + state.storage[usize(index)] = Storage{ kind = .local, index = u32(index_slot) } } try predeclare_locals(state, loop.body) } @@ -604,8 +575,7 @@ lower_logical proc(state @mut State, id ast.NodeId) ir.ValueId ! (mem.AllocError left :: try lower_expression(state, expression.left) if (state.terminated) return ir.no_value origin :: state.block - slot :: u32(state.module.functions.items[state.function].locals.items.len) - try arraylist.append(&state.module.functions.items[state.function].locals, ir.Type.bool) + slot :: u32(try ir.add_local(state.module, ir.FunctionId(state.function), .bool, ir.no_array)) right_block :: try add_block(state) skip :: try add_block(state) join :: try add_block(state) diff --git a/source/main.hon b/source/main.hon index ed111ea..a6004a0 100644 --- a/source/main.hon +++ b/source/main.hon @@ -32,6 +32,7 @@ test import "@source/ast" test import "@source/parser" test import "@source/sema" test import "@source/sema/check" +test import "@source/ir" test import "@source/lower" test import "@source/interpreter"