[Flint] adding IR lowering and printer
Mirror Gitea refs to GitHub / mirror (push) Successful in 18s
Test / build_and_test (push) Successful in 6m17s
Build / build (push) Successful in 7m51s

[IR] switching from "passes" to "transformers" for clarity
This commit is contained in:
2026-07-29 15:39:41 +02:00
parent 045497b264
commit 948e8b86a3
27 changed files with 3453 additions and 1816 deletions
+218 -115
View File
@@ -3,44 +3,42 @@ const ids = @import("../id.zig");
const type_ir = @import("../type.zig");
const inst_ir = @import("../instruction.zig");
const module_ir = @import("../module.zig");
const Builder = @import("../Builder.zig");
const dominance = @import("dominance.zig");
pub const ValidationError = error{
MissingEntryPoint,
InvalidEntryPoint,
InvalidType,
InvalidConstant,
InvalidValue,
InvalidFunction,
CrossFunctionReference,
DefinitionDoesNotDominateUse,
EntryBlockHasPredecessor,
InvalidBlock,
InvalidConstant,
InvalidEntryPoint,
InvalidFunction,
InvalidInstruction,
InvalidStructuredControl,
InvalidType,
InvalidValue,
MissingEntryPoint,
MissingFunctionEntryBlock,
MissingTerminator,
EntryBlockHasPredecessor,
WrongParent,
WrongDefinition,
WrongParameterIndex,
WrongResultPresence,
WrongOperandType,
WrongResultType,
WrongBranchArgumentCount,
WrongBranchArgumentType,
CrossFunctionReference,
WrongReturnType,
WrongDefinition,
WrongInterfaceDirection,
InvalidStructuredControl,
DefinitionDoesNotDominateUse,
WrongOperandType,
WrongParameterIndex,
WrongParent,
WrongResultPresence,
WrongResultType,
WrongReturnType,
};
pub const Error = ValidationError || std.mem.Allocator.Error;
/// Early validator for the foundational IR. It covers object ownership, CFG
/// edges, single definitions, function boundaries, and the currently modeled
/// operation types, and SSA dominance.
pub fn validate(module: *const module_ir.Module) Error!void {
const entry_point = module.entry_point orelse return error.MissingEntryPoint;
const entry_point = module.entry_point orelse return Error.MissingEntryPoint;
if (!module.functions.isLive(entry_point))
return error.InvalidEntryPoint;
return Error.InvalidEntryPoint;
for (module.types.entries.items) |entry| {
const ty = entry orelse continue;
@@ -51,12 +49,12 @@ pub fn validate(module: *const module_ir.Module) Error!void {
const constant = entry orelse continue;
if (!module.types.isLive(constant.type))
return error.InvalidType;
return Error.InvalidType;
if (constant.value == .composite) {
for (constant.value.composite) |element| {
if (!module.constants.isLive(element))
return error.InvalidConstant;
return Error.InvalidConstant;
}
}
}
@@ -65,30 +63,30 @@ pub fn validate(module: *const module_ir.Module) Error!void {
const value = entry orelse continue;
if (!module.types.isLive(value.type))
return error.InvalidType;
return Error.InvalidType;
const value_id = ids.ValueId.fromIndex(value_index);
switch (value.definition) {
.constant => |id| {
const constant = module.constants.get(id) orelse return error.InvalidConstant;
const constant = module.constants.get(id) orelse return Error.InvalidConstant;
if (constant.type != value.type)
return error.WrongResultType;
return Error.WrongResultType;
},
.function_parameter => |definition| {
const function = module.functions.get(definition.function) orelse return error.InvalidFunction;
const function = module.functions.get(definition.function) orelse return Error.InvalidFunction;
if (definition.index >= function.parameters.items.len or function.parameters.items[definition.index] != value_id)
return error.WrongParameterIndex;
return Error.WrongParameterIndex;
},
.block_parameter => |definition| {
const block = module.blocks.get(definition.block) orelse return error.InvalidBlock;
const block = module.blocks.get(definition.block) orelse return Error.InvalidBlock;
if (definition.index >= block.parameters.items.len or block.parameters.items[definition.index] != value_id)
return error.WrongParameterIndex;
return Error.WrongParameterIndex;
},
.instruction => |instruction_id| {
const instruction = module.instructions.get(instruction_id) orelse return error.InvalidInstruction;
const instruction = module.instructions.get(instruction_id) orelse return Error.InvalidInstruction;
if (instruction.result != value_id)
return error.WrongDefinition;
return Error.WrongDefinition;
},
.undef => {},
}
@@ -97,13 +95,13 @@ pub fn validate(module: *const module_ir.Module) Error!void {
for (module.interface_variables.entries.items) |entry| {
const variable = entry orelse continue;
if (!module.types.isLive(variable.type))
return error.InvalidType;
return Error.InvalidType;
}
for (module.resources.entries.items) |entry| {
const resource = entry orelse continue;
if (!module.types.isLive(resource.type))
return error.InvalidType;
return Error.InvalidType;
}
for (module.functions.entries.items, 0..) |entry, function_index| {
@@ -111,32 +109,32 @@ pub fn validate(module: *const module_ir.Module) Error!void {
const function_id = ids.FunctionId.fromIndex(function_index);
if (!module.types.isLive(function.return_type))
return error.InvalidType;
return Error.InvalidType;
if (function.parameter_types.items.len != function.parameters.items.len)
return error.WrongParameterIndex;
return Error.WrongParameterIndex;
for (function.parameter_types.items, function.parameters.items, 0..) |parameter_type, parameter_id, index| {
const parameter = module.values.get(parameter_id) orelse return error.InvalidValue;
const parameter = module.values.get(parameter_id) orelse return Error.InvalidValue;
if (parameter.type != parameter_type)
return error.WrongResultType;
return Error.WrongResultType;
if (parameter.definition != .function_parameter or
parameter.definition.function_parameter.function != function_id or
parameter.definition.function_parameter.index != index)
return error.WrongDefinition;
return Error.WrongDefinition;
}
const entry_block = function.entry_block orelse return error.MissingFunctionEntryBlock;
const entry_block_value = module.blocks.get(entry_block) orelse return error.InvalidBlock;
if (entry_block_value.parent_function != function_id) return error.WrongParent;
const entry_block = function.entry_block orelse return Error.MissingFunctionEntryBlock;
const entry_block_value = module.blocks.get(entry_block) orelse return Error.InvalidBlock;
if (entry_block_value.parent_function != function_id) return Error.WrongParent;
for (function.blocks.items) |block_id| {
const block = module.blocks.get(block_id) orelse return error.InvalidBlock;
const block = module.blocks.get(block_id) orelse return Error.InvalidBlock;
if (block.parent_function != function_id)
return error.WrongParent;
return Error.WrongParent;
try validateBlock(module, function_id, block_id, block);
}
@@ -146,7 +144,7 @@ pub fn validate(module: *const module_ir.Module) Error!void {
if (block.terminator) |terminator| {
if (targetsBlock(terminator, entry_block))
return error.EntryBlockHasPredecessor;
return Error.EntryBlockHasPredecessor;
}
}
@@ -158,23 +156,23 @@ fn validateType(module: *const module_ir.Module, ty: type_ir.Type) ValidationErr
switch (ty) {
.vector => |vector| {
if (!module.types.isLive(vector.element_type) or vector.length < 2)
return error.InvalidType;
return ValidationError.InvalidType;
},
.array => |array| {
if (!module.types.isLive(array.element_type) or array.length == 0)
return error.InvalidType;
return ValidationError.InvalidType;
},
.structure => |structure| for (structure.members) |member| {
if (!module.types.isLive(member))
return error.InvalidType;
return ValidationError.InvalidType;
},
.pointer => |pointer| {
if (!module.types.isLive(pointer.pointee_type))
return error.InvalidType;
return ValidationError.InvalidType;
},
.resource_handle => |handle| if (handle.data_type) |data_type| {
if (!module.types.isLive(data_type))
return error.InvalidType;
return ValidationError.InvalidType;
},
else => {},
}
@@ -187,11 +185,11 @@ fn validateBlock(
block: *const module_ir.Block,
) ValidationError!void {
for (block.parameters.items, 0..) |parameter_id, index| {
const parameter = module.values.get(parameter_id) orelse return error.InvalidValue;
const parameter = module.values.get(parameter_id) orelse return ValidationError.InvalidValue;
if (parameter.definition != .block_parameter or
parameter.definition.block_parameter.block != block_id or
parameter.definition.block_parameter.index != index)
return error.WrongDefinition;
return ValidationError.WrongDefinition;
}
switch (block.structured_control) {
@@ -204,97 +202,97 @@ fn validateBlock(
}
for (block.instructions.items) |instruction_id| {
const instruction = module.instructions.get(instruction_id) orelse return error.InvalidInstruction;
const instruction = module.instructions.get(instruction_id) orelse return ValidationError.InvalidInstruction;
if (instruction.parent_block != block_id)
return error.WrongParent;
return ValidationError.WrongParent;
if (instruction.result) |result_id| {
const result = module.values.get(result_id) orelse return error.InvalidValue;
const result = module.values.get(result_id) orelse return ValidationError.InvalidValue;
if (result.definition != .instruction or result.definition.instruction != instruction_id)
return error.WrongDefinition;
return ValidationError.WrongDefinition;
}
try validateOperation(module, function_id, instruction);
}
const terminator = block.terminator orelse return error.MissingTerminator;
const terminator = block.terminator orelse return ValidationError.MissingTerminator;
try validateTerminator(module, function_id, terminator);
}
fn validateOperation(module: *const module_ir.Module, function_id: ids.FunctionId, instruction: *const inst_ir.Instruction) ValidationError!void {
const result_type = if (instruction.result) |result| module.typeOf(result) orelse return error.InvalidValue else null;
const result_type = if (instruction.result) |result| module.typeOf(result) orelse return ValidationError.InvalidValue else null;
switch (instruction.operation) {
.unary => |op| {
const operand_type = try operandType(module, function_id, op.operand);
if (result_type == null)
return error.WrongResultPresence;
return ValidationError.WrongResultPresence;
if (result_type.? != operand_type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.binary => |op| {
const lhs_type = try operandType(module, function_id, op.lhs);
const rhs_type = try operandType(module, function_id, op.rhs);
if (lhs_type != rhs_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
if (result_type == null or result_type.? != lhs_type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.compare => |op| {
const lhs_type = try operandType(module, function_id, op.lhs);
if (try operandType(module, function_id, op.rhs) != lhs_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
const result = result_type orelse return error.WrongResultPresence;
const result = result_type orelse return ValidationError.WrongResultPresence;
if (!isBoolean(module, result))
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.select => |op| {
if (!isBoolean(module, try operandType(module, function_id, op.condition)))
return error.WrongOperandType;
return ValidationError.WrongOperandType;
const true_type = try operandType(module, function_id, op.true_value);
if (try operandType(module, function_id, op.false_value) != true_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
if (result_type == null or result_type.? != true_type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.bitcast => |operand| {
_ = try operandType(module, function_id, operand);
if (result_type == null)
return error.WrongResultPresence;
return ValidationError.WrongResultPresence;
},
.composite_construct => |op| {
const result = result_type orelse return error.WrongResultPresence;
const ty = module.types.get(result) orelse return error.InvalidType;
const result = result_type orelse return ValidationError.WrongResultPresence;
const ty = module.types.get(result) orelse return ValidationError.InvalidType;
switch (ty.*) {
.vector => |vector| {
if (op.elements.len != vector.length)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
for (op.elements) |element| {
if (try operandType(module, function_id, element) != vector.element_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
}
},
.structure => |structure| {
if (op.elements.len != structure.members.len)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
for (op.elements, structure.members) |element, member_type| {
if (try operandType(module, function_id, element) != member_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
}
},
else => return error.WrongResultType,
else => return ValidationError.WrongResultType,
}
},
.composite_extract => |op| {
@@ -302,127 +300,127 @@ fn validateOperation(module: *const module_ir.Module, function_id: ids.FunctionI
const extracted_type = try indexedType(module, composite_type, op.indices);
if (result_type == null or result_type.? != extracted_type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.load_interface => |op| {
const variable = module.interface_variables.get(op.variable) orelse return error.InvalidValue;
const variable = module.interface_variables.get(op.variable) orelse return ValidationError.InvalidValue;
if (variable.direction != .input)
return error.WrongInterfaceDirection;
return ValidationError.WrongInterfaceDirection;
if (op.element_index) |index|
_ = try operandType(module, function_id, index);
if (result_type == null or result_type.? != variable.type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
.store_interface => |op| {
if (result_type != null)
return error.WrongResultPresence;
return ValidationError.WrongResultPresence;
const variable = module.interface_variables.get(op.variable) orelse return error.InvalidValue;
const variable = module.interface_variables.get(op.variable) orelse return ValidationError.InvalidValue;
if (variable.direction != .output)
return error.WrongInterfaceDirection;
return ValidationError.WrongInterfaceDirection;
if (try operandType(module, function_id, op.value) != variable.type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
if (op.element_index) |index|
_ = try operandType(module, function_id, index);
},
.call => |op| {
const callee = module.functions.get(op.function) orelse return error.InvalidFunction;
const callee = module.functions.get(op.function) orelse return ValidationError.InvalidFunction;
if (op.arguments.len != callee.parameter_types.items.len)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
for (op.arguments, callee.parameter_types.items) |argument, parameter_type| {
if (try operandType(module, function_id, argument) != parameter_type)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
}
const return_type = module.types.get(callee.return_type) orelse return error.InvalidType;
const return_type = module.types.get(callee.return_type) orelse return ValidationError.InvalidType;
if (return_type.* == .void) {
if (result_type != null)
return error.WrongResultPresence;
return ValidationError.WrongResultPresence;
} else if (result_type == null or result_type.? != callee.return_type)
return error.WrongResultType;
return ValidationError.WrongResultType;
},
}
}
fn validateTerminator(module: *const module_ir.Module, function_id: ids.FunctionId, terminator: module_ir.Terminator) ValidationError!void {
const function = module.functions.get(function_id) orelse return error.InvalidFunction;
const function = module.functions.get(function_id) orelse return ValidationError.InvalidFunction;
switch (terminator) {
.branch => |edge| try validateEdge(module, function_id, edge),
.conditional_branch => |branch| {
if (!isBoolean(module, try operandType(module, function_id, branch.condition)))
return error.WrongOperandType;
return ValidationError.WrongOperandType;
try validateEdge(module, function_id, branch.true_edge);
try validateEdge(module, function_id, branch.false_edge);
},
.return_void => {
if (module.types.get(function.return_type).?.* != .void)
return error.WrongReturnType;
return ValidationError.WrongReturnType;
},
.return_value => |value| {
if (try operandType(module, function_id, value) != function.return_type)
return error.WrongReturnType;
return ValidationError.WrongReturnType;
},
.discard => {
if (module.stage != .fragment)
return error.WrongReturnType;
return ValidationError.WrongReturnType;
},
.@"unreachable" => {},
}
}
fn validateEdge(module: *const module_ir.Module, function_id: ids.FunctionId, edge: module_ir.Edge) ValidationError!void {
const target = module.blocks.get(edge.target) orelse return error.InvalidBlock;
const target = module.blocks.get(edge.target) orelse return ValidationError.InvalidBlock;
if (target.parent_function != function_id)
return error.CrossFunctionReference;
return ValidationError.CrossFunctionReference;
if (edge.arguments.len != target.parameters.items.len)
return error.WrongBranchArgumentCount;
return ValidationError.WrongBranchArgumentCount;
for (edge.arguments, target.parameters.items) |argument, parameter| {
if (try operandType(module, function_id, argument) != module.typeOf(parameter).?)
return error.WrongBranchArgumentType;
return ValidationError.WrongBranchArgumentType;
}
}
fn validateTarget(module: *const module_ir.Module, function_id: ids.FunctionId, target_id: ids.BlockId) ValidationError!void {
const target = module.blocks.get(target_id) orelse return error.InvalidStructuredControl;
const target = module.blocks.get(target_id) orelse return ValidationError.InvalidStructuredControl;
if (target.parent_function != function_id)
return error.InvalidStructuredControl;
return ValidationError.InvalidStructuredControl;
}
fn operandType(module: *const module_ir.Module, function_id: ids.FunctionId, value_id: ids.ValueId) ValidationError!ids.TypeId {
const value = module.values.get(value_id) orelse return error.InvalidValue;
const owner = valueFunction(module, value_id) catch return error.InvalidValue;
const value = module.values.get(value_id) orelse return ValidationError.InvalidValue;
const owner = valueFunction(module, value_id) catch return ValidationError.InvalidValue;
if (owner) |actual| {
if (actual != function_id)
return error.CrossFunctionReference;
return ValidationError.CrossFunctionReference;
}
return value.type;
}
fn valueFunction(module: *const module_ir.Module, value_id: ids.ValueId) ValidationError!?ids.FunctionId {
const value = module.values.get(value_id) orelse return error.InvalidValue;
const value = module.values.get(value_id) orelse return ValidationError.InvalidValue;
return switch (value.definition) {
.constant, .undef => null,
.function_parameter => |definition| definition.function,
.block_parameter => |definition| (module.blocks.get(definition.block) orelse return error.InvalidBlock).parent_function,
.block_parameter => |definition| (module.blocks.get(definition.block) orelse return ValidationError.InvalidBlock).parent_function,
.instruction => |instruction_id| blk: {
const instruction = module.instructions.get(instruction_id) orelse return error.InvalidInstruction;
const block = module.blocks.get(instruction.parent_block) orelse return error.InvalidBlock;
const instruction = module.instructions.get(instruction_id) orelse return ValidationError.InvalidInstruction;
const block = module.blocks.get(instruction.parent_block) orelse return ValidationError.InvalidBlock;
break :blk block.parent_function;
},
};
@@ -430,27 +428,27 @@ fn valueFunction(module: *const module_ir.Module, value_id: ids.ValueId) Validat
fn indexedType(module: *const module_ir.Module, root: ids.TypeId, indices: []const u32) ValidationError!ids.TypeId {
if (indices.len == 0)
return error.WrongOperandType;
return ValidationError.WrongOperandType;
var current = root;
for (indices) |index| {
const ty = module.types.get(current) orelse return error.InvalidType;
const ty = module.types.get(current) orelse return ValidationError.InvalidType;
current = switch (ty.*) {
.vector => |vector| if (index < vector.length)
vector.element_type
else
return error.WrongOperandType,
return ValidationError.WrongOperandType,
.array => |array| if (index < array.length)
array.element_type
else
return error.WrongOperandType,
return ValidationError.WrongOperandType,
.structure => |structure| if (index < structure.members.len)
structure.members[index]
else
return error.WrongOperandType,
return ValidationError.WrongOperandType,
else => return error.WrongOperandType,
else => return ValidationError.WrongOperandType,
};
}
return current;
@@ -468,3 +466,108 @@ fn targetsBlock(terminator: module_ir.Terminator, target: ids.BlockId) bool {
else => false,
};
}
test "Validator: Error wrong block argument count" {
// shader compute @main
// {
// fn @main() -> void
// {
// .entry():
// branch .merge()
//
// .merge(%0: u32):
// return
// }
// }
var module = module_ir.Module.init(std.testing.allocator, .compute);
defer module.deinit();
var builder = Builder.init(&module);
const void_type = try builder.internType(.void);
const u32_type = try builder.internType(.{ .integer = .{ .bits = 32, .signedness = .unsigned } });
const main = try builder.addFunction(void_type, "main");
builder.setEntryPoint(main);
const entry = try builder.addBlock(main, "entry");
const merge = try builder.addBlock(main, "merge");
_ = try builder.addBlockParameter(merge, u32_type, null);
try builder.setTerminator(entry, .{ .branch = try builder.edge(merge, &.{}) });
try builder.setTerminator(merge, .return_void);
try std.testing.expectError(Error.WrongBranchArgumentCount, validate(&module));
}
test "Validator: Error SSA definition does not dominate its use" {
// shader compute @main
// {
// %0: constant bool = true
// %1: constant u32 = bits(0x1)
//
// fn @main() -> void
// {
// .entry():
// conditional_branch %0, .left(), .right()
//
// .left():
// %2: u32 = integer_add %1, %1
// branch .merge()
//
// .right():
// branch .merge()
//
// .merge():
// %3: u32 = integer_multiply %2, %1
// return
// }
// }
var module = module_ir.Module.init(std.testing.allocator, .compute);
defer module.deinit();
var builder = Builder.init(&module);
const void_type = try builder.internType(.void);
const bool_type = try builder.internType(.boolean);
const u32_type = try builder.internType(.{ .integer = .{ .bits = 32, .signedness = .unsigned } });
const condition = try builder.internConstant(bool_type, .{ .boolean = true });
const one = try builder.internConstant(u32_type, .{ .integer_bits = 1 });
const main = try builder.addFunction(void_type, "main");
builder.setEntryPoint(main);
const entry = try builder.addBlock(main, "entry");
const left = try builder.addBlock(main, "left");
const right = try builder.addBlock(main, "right");
const merge = try builder.addBlock(main, "merge");
try builder.setTerminator(
entry,
.{
.conditional_branch = .{
.condition = condition,
.true_edge = try builder.edge(left, &.{}),
.false_edge = try builder.edge(right, &.{}),
},
},
);
const left_value = (try builder.appendInstruction(left, u32_type, .{
.binary = .{
.opcode = .integer_add,
.lhs = one,
.rhs = one,
},
}, null)).?;
try builder.setTerminator(left, .{ .branch = try builder.edge(merge, &.{}) });
try builder.setTerminator(right, .{ .branch = try builder.edge(merge, &.{}) });
_ = try builder.appendInstruction(merge, u32_type, .{
.binary = .{
.opcode = .integer_multiply,
.lhs = left_value,
.rhs = one,
},
}, null);
try builder.setTerminator(merge, .return_void);
try std.testing.expectError(Error.DefinitionDoesNotDominateUse, validate(&module));
}