refactor: centralize IR construction invariants
This commit is contained in:
+123
-1
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user