Files
VulkanDriver/src/compiler/ir/validator/validator.zig
T
kbz_8 9c4b074622
Mirror Gitea refs to GitHub / mirror (push) Successful in 16s
Test / build_and_test (push) Failing after 1m42s
Build / build (push) Successful in 5m2s
[IR] adding OpNot and fixing access chain handling
2026-08-30 02:21:27 +02:00

1230 lines
41 KiB
Zig

const std = @import("std");
const ids = @import("../id.zig");
const type_ir = @import("../type.zig");
const inst_ir = @import("../instruction.zig");
const module_ir = @import("../module.zig");
const dominance = @import("dominance.zig");
pub const ValidationError = error{
CrossFunctionReference,
DefinitionDoesNotDominateUse,
EntryBlockHasPredecessor,
InvalidBlock,
InvalidConstant,
InvalidEntryPoint,
InvalidFunction,
InvalidInstruction,
InvalidStructuredControl,
InvalidType,
InvalidValue,
MissingEntryPoint,
MissingFunctionEntryBlock,
MissingTerminator,
WrongBranchArgumentCount,
WrongBranchArgumentType,
WrongDefinition,
WrongInterfaceDirection,
WrongOperandType,
WrongParameterIndex,
WrongParent,
WrongResourceKind,
WrongResultPresence,
WrongResultType,
WrongReturnType,
};
const IntegerShape = struct {
bits: u16,
components: u8,
};
pub const Error = ValidationError || std.mem.Allocator.Error;
pub fn validate(module: *const module_ir.Module) Error!void {
const entry_point = module.entry_point orelse return Error.MissingEntryPoint;
if (!module.functions.isLive(entry_point))
return Error.InvalidEntryPoint;
for (module.types.entries.items) |entry| {
const ty = entry orelse continue;
try validateType(module, ty);
}
for (module.constants.entries.items) |entry| {
const constant = entry orelse continue;
if (!module.types.isLive(constant.type))
return Error.InvalidType;
if (constant.value == .composite) {
for (constant.value.composite) |element| {
if (!module.constants.isLive(element))
return Error.InvalidConstant;
}
}
}
for (module.values.entries.items, 0..) |entry, value_index| {
const value = entry orelse continue;
if (!module.types.isLive(value.type))
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;
if (constant.type != value.type)
return Error.WrongResultType;
},
.function_parameter => |definition| {
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;
},
.block_parameter => |definition| {
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;
},
.instruction => |instruction_id| {
const instruction = module.instructions.get(instruction_id) orelse return Error.InvalidInstruction;
if (instruction.result != value_id)
return Error.WrongDefinition;
},
.undef => {},
}
}
for (module.interface_variables.entries.items) |entry| {
const variable = entry orelse continue;
if (!module.types.isLive(variable.type))
return Error.InvalidType;
}
for (module.resources.entries.items) |entry| {
const resource = entry orelse continue;
if (!module.types.isLive(resource.type))
return Error.InvalidType;
}
for (module.functions.entries.items, 0..) |entry, function_index| {
const function = entry orelse continue;
const function_id = ids.FunctionId.fromIndex(function_index);
if (!module.types.isLive(function.return_type))
return Error.InvalidType;
if (function.parameter_types.items.len != function.parameters.items.len)
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;
if (parameter.type != parameter_type)
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;
}
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;
if (block.parent_function != function_id)
return Error.WrongParent;
try validateBlock(module, function_id, block_id, block);
}
for (function.blocks.items) |block_id| {
const block = module.blocks.get(block_id).?;
if (block.terminator) |terminator| {
if (targetsBlock(terminator, entry_block))
return Error.EntryBlockHasPredecessor;
}
}
try dominance.validate(module, function_id);
}
}
fn validateType(module: *const module_ir.Module, ty: type_ir.Type) ValidationError!void {
switch (ty) {
.vector => |vector| {
if (!module.types.isLive(vector.element_type) or vector.length < 2)
return ValidationError.InvalidType;
},
.array => |array| {
if (!module.types.isLive(array.element_type) or array.length == 0)
return ValidationError.InvalidType;
},
.structure => |structure| for (structure.members) |member| {
if (!module.types.isLive(member))
return ValidationError.InvalidType;
},
.pointer => |pointer| {
if (!module.types.isLive(pointer.pointee_type))
return ValidationError.InvalidType;
},
.resource_handle => |handle| if (handle.data_type) |data_type| {
if (!module.types.isLive(data_type))
return ValidationError.InvalidType;
},
else => {},
}
}
fn validateBlock(
module: *const module_ir.Module,
function_id: ids.FunctionId,
block_id: ids.BlockId,
block: *const module_ir.Block,
) ValidationError!void {
for (block.parameters.items, 0..) |parameter_id, index| {
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 ValidationError.WrongDefinition;
}
switch (block.structured_control) {
.none => {},
.selection => |selection| try validateTarget(module, function_id, selection.merge_block),
.loop => |loop| {
try validateTarget(module, function_id, loop.merge_block);
try validateTarget(module, function_id, loop.continue_block);
},
}
for (block.instructions.items) |instruction_id| {
const instruction = module.instructions.get(instruction_id) orelse return ValidationError.InvalidInstruction;
if (instruction.parent_block != block_id)
return ValidationError.WrongParent;
if (instruction.result) |result_id| {
const result = module.values.get(result_id) orelse return ValidationError.InvalidValue;
if (result.definition != .instruction or result.definition.instruction != instruction_id)
return ValidationError.WrongDefinition;
}
try validateOperation(module, function_id, instruction);
}
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 ValidationError.InvalidValue else null;
switch (instruction.operation) {
.unary => |op| {
const operand_type =
try operandType(module, function_id, op.operand);
const result =
result_type orelse return ValidationError.WrongResultPresence;
switch (op.opcode) {
.bitwise_not => {
if (!isIntegerScalarOrVector(module, operand_type))
return ValidationError.WrongOperandType;
if (!isIntegerScalarOrVector(module, result))
return ValidationError.WrongResultType;
if (!haveSameIntegerShape(module, operand_type, result))
return ValidationError.WrongResultType;
},
.logical_not => {
if (!isBoolean(module, operand_type))
return ValidationError.WrongOperandType;
if (!isBoolean(module, result))
return ValidationError.WrongResultType;
},
.negate => {
if (result != operand_type)
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 ValidationError.WrongOperandType;
if (result_type == null or result_type.? != lhs_type)
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 ValidationError.WrongOperandType;
const result = result_type orelse return ValidationError.WrongResultPresence;
if (!isBoolean(module, result))
return ValidationError.WrongResultType;
},
.select => |op| {
if (!isBoolean(module, try operandType(module, function_id, op.condition)))
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 ValidationError.WrongOperandType;
if (result_type == null or result_type.? != true_type)
return ValidationError.WrongResultType;
},
.bitcast => |operand| {
_ = try operandType(module, function_id, operand);
if (result_type == null)
return ValidationError.WrongResultPresence;
},
.composite_construct => |op| {
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 ValidationError.WrongOperandType;
for (op.elements) |element| {
if (try operandType(module, function_id, element) != vector.element_type)
return ValidationError.WrongOperandType;
}
},
.structure => |structure| {
if (op.elements.len != structure.members.len)
return ValidationError.WrongOperandType;
for (op.elements, structure.members) |element, member_type| {
if (try operandType(module, function_id, element) != member_type)
return ValidationError.WrongOperandType;
}
},
else => return ValidationError.WrongResultType,
}
},
.composite_extract => |op| {
const composite_type = try operandType(module, function_id, op.composite);
const extracted_type = try indexedType(module, composite_type, op.indices);
if (result_type == null or result_type.? != extracted_type)
return ValidationError.WrongResultType;
},
.load_interface => |op| {
const variable = module.interface_variables.get(op.variable) orelse return ValidationError.InvalidValue;
if (variable.direction != .input)
return ValidationError.WrongInterfaceDirection;
if (op.element_index) |index|
_ = try operandType(module, function_id, index);
if (result_type == null or result_type.? != variable.type)
return ValidationError.WrongResultType;
},
.store_interface => |op| {
if (result_type != null)
return ValidationError.WrongResultPresence;
const variable = module.interface_variables.get(op.variable) orelse return ValidationError.InvalidValue;
if (variable.direction != .output)
return ValidationError.WrongInterfaceDirection;
if (try operandType(module, function_id, op.value) != variable.type)
return ValidationError.WrongOperandType;
if (op.element_index) |index|
_ = try operandType(module, function_id, index);
},
.load_buffer => |op| {
const resource = module.resources.get(op.resource) orelse return ValidationError.InvalidValue;
if (resource.kind != .storage_buffer)
return ValidationError.WrongResourceKind;
if (!isUnsignedInteger(module, try operandType(module, function_id, op.byte_offset)))
return ValidationError.WrongOperandType;
const result = result_type orelse return ValidationError.WrongResultPresence;
if (!isBufferAccessibleType(module, result))
return ValidationError.WrongResultType;
},
.store_buffer => |op| {
if (result_type != null)
return ValidationError.WrongResultPresence;
const resource = module.resources.get(op.resource) orelse return ValidationError.InvalidValue;
if (resource.kind != .storage_buffer)
return ValidationError.WrongResourceKind;
if (!isUnsignedInteger(module, try operandType(module, function_id, op.byte_offset)))
return ValidationError.WrongOperandType;
if (!isBufferAccessibleType(module, try operandType(module, function_id, op.value)))
return ValidationError.WrongOperandType;
},
.call => |op| {
const callee = module.functions.get(op.function) orelse return ValidationError.InvalidFunction;
if (op.arguments.len != callee.parameter_types.items.len)
return ValidationError.WrongOperandType;
for (op.arguments, callee.parameter_types.items) |argument, parameter_type| {
if (try operandType(module, function_id, argument) != parameter_type)
return ValidationError.WrongOperandType;
}
const return_type = module.types.get(callee.return_type) orelse return ValidationError.InvalidType;
if (return_type.* == .void) {
if (result_type != null)
return ValidationError.WrongResultPresence;
} else if (result_type == null or result_type.? != callee.return_type)
return ValidationError.WrongResultType;
},
.array_length => |op| {
const resource = module.resources.get(op.resource) orelse return ValidationError.InvalidValue;
if (resource.kind != .storage_buffer)
return ValidationError.WrongResourceKind;
if (!isUnsignedInteger(module, try operandType(module, function_id, op.byte_offset)))
return ValidationError.WrongOperandType;
if (op.stride == 0)
return ValidationError.InvalidInstruction;
const result = result_type orelse return ValidationError.WrongResultPresence;
if (!isArrayLengthResultType(module, result))
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 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 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 ValidationError.WrongReturnType;
},
.return_value => |value| {
if (try operandType(module, function_id, value) != function.return_type)
return ValidationError.WrongReturnType;
},
.discard => {
if (module.stage != .fragment)
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 ValidationError.InvalidBlock;
if (target.parent_function != function_id)
return ValidationError.CrossFunctionReference;
if (edge.arguments.len != target.parameters.items.len)
return ValidationError.WrongBranchArgumentCount;
for (edge.arguments, target.parameters.items) |argument, parameter| {
if (try operandType(module, function_id, argument) != module.typeOf(parameter).?)
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 ValidationError.InvalidStructuredControl;
if (target.parent_function != function_id)
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 ValidationError.InvalidValue;
const owner = valueFunction(module, value_id) catch return ValidationError.InvalidValue;
if (owner) |actual| {
if (actual != function_id)
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 ValidationError.InvalidValue;
return switch (value.definition) {
.constant, .undef => null,
.function_parameter => |definition| definition.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 ValidationError.InvalidInstruction;
const block = module.blocks.get(instruction.parent_block) orelse return ValidationError.InvalidBlock;
break :blk block.parent_function;
},
};
}
fn indexedType(module: *const module_ir.Module, root: ids.TypeId, indices: []const u32) ValidationError!ids.TypeId {
if (indices.len == 0)
return ValidationError.WrongOperandType;
var current = root;
for (indices) |index| {
const ty = module.types.get(current) orelse return ValidationError.InvalidType;
current = switch (ty.*) {
.vector => |vector| if (index < vector.length)
vector.element_type
else
return ValidationError.WrongOperandType,
.array => |array| if (index < array.length)
array.element_type
else
return ValidationError.WrongOperandType,
.structure => |structure| if (index < structure.members.len)
structure.members[index]
else
return ValidationError.WrongOperandType,
else => return ValidationError.WrongOperandType,
};
}
return current;
}
fn isBoolean(module: *const module_ir.Module, type_id: ids.TypeId) bool {
const ty = module.types.get(type_id) orelse return false;
return ty.* == .boolean;
}
fn isUnsignedInteger(module: *const module_ir.Module, type_id: ids.TypeId) bool {
const ty = module.types.get(type_id) orelse return false;
return switch (ty.*) {
.integer => |integer| integer.signedness == .unsigned,
else => false,
};
}
fn isBufferAccessibleType(module: *const module_ir.Module, type_id: ids.TypeId) bool {
const ty = module.types.get(type_id) orelse return false;
return switch (ty.*) {
.integer, .floating => true,
.vector => |vector| {
const element_type = module.types.get(vector.element_type) orelse return false;
return element_type.* == .integer or element_type.* == .floating;
},
else => false,
};
}
fn isArrayLengthResultType(module: *const module_ir.Module, type_id: ids.TypeId) bool {
const ty = module.types.get(type_id) orelse return false;
return switch (ty.*) {
.integer => |integer| integer.signedness == .unsigned and (integer.bits == 32 or integer.bits == 64),
else => false,
};
}
fn integerShape(module: *const module_ir.Module, type_id: ids.TypeId) ?IntegerShape {
const ty = module.types.get(type_id) orelse return null;
return switch (ty.*) {
.integer => |integer| .{
.bits = integer.bits,
.components = 1,
},
.vector => |vector| blk: {
const element =
module.types.get(vector.element_type) orelse return null;
const integer = switch (element.*) {
.integer => |integer| integer,
else => return null,
};
break :blk .{
.bits = integer.bits,
.components = vector.length,
};
},
else => null,
};
}
fn isIntegerScalarOrVector(module: *const module_ir.Module, type_id: ids.TypeId) bool {
return integerShape(module, type_id) != null;
}
fn haveSameIntegerShape(module: *const module_ir.Module, lhs: ids.TypeId, rhs: ids.TypeId) bool {
const lhs_shape = integerShape(module, lhs) orelse return false;
const rhs_shape = integerShape(module, rhs) orelse return false;
return lhs_shape.bits == rhs_shape.bits and
lhs_shape.components == rhs_shape.components;
}
fn targetsBlock(terminator: module_ir.Terminator, target: ids.BlockId) bool {
return switch (terminator) {
.branch => |edge| edge.target == target,
.conditional_branch => |branch| branch.true_edge.target == target or branch.false_edge.target == target,
else => false,
};
}
fn expectValidationError(expected: Error, source: []const u8) !void {
const parser = @import("../parser/parser.zig");
try std.testing.expectError(expected, parser.parseString(std.testing.allocator, source));
}
test "Validator: well-typed control flow and operations" {
const parser = @import("../parser/parser.zig");
var module = try parser.parseString(std.testing.allocator,
\\shader vertex @main
\\{
\\ @input: u32 = input[location(0), component(0), index(0)]
\\ @output: u32 = output[location(0), component(0), index(0)]
\\ %condition: constant bool = true
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %loaded: u32 = load_interface @input
\\ conditional_branch %condition, .left(%loaded), .right(%one)
\\ .left(%left_value: u32):
\\ %left_result: u32 = integer_add %left_value, %one
\\ branch .merge(%left_result)
\\ .right(%right_value: u32):
\\ %right_result: u32 = call @identity(%right_value)
\\ branch .merge(%right_result)
\\ .merge(%result: u32):
\\ store_interface @output, %result
\\ return
\\ }
\\ fn @identity(%value: u32) -> u32
\\ {
\\ .entry():
\\ return %value
\\ }
\\}
);
defer module.deinit();
try validate(&module);
}
test "Validator: required entry point" {
try expectValidationError(Error.MissingEntryPoint,
\\shader compute
\\{
\\ fn @helper() -> void
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
}
test "Validator: required function entry block" {
try expectValidationError(Error.MissingFunctionEntryBlock,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ }
\\}
);
}
test "Validator: reject invalid aggregate types" {
try expectValidationError(Error.InvalidType,
\\shader compute @main
\\{
\\ fn @main(%value: vec1[u32]) -> void
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
try expectValidationError(Error.InvalidType,
\\shader compute @main
\\{
\\ fn @main(%value: array[u32, 0]) -> void
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
}
test "Validator: reject predecessor of the entry block" {
try expectValidationError(Error.EntryBlockHasPredecessor,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ return
\\ .back_edge():
\\ branch .entry()
\\ }
\\}
);
}
test "Validator: check branch arguments" {
try expectValidationError(Error.WrongBranchArgumentCount,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ branch .merge()
\\ .merge(%value: u32):
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongBranchArgumentType,
\\shader compute @main
\\{
\\ %condition: constant bool = true
\\ fn @main() -> void
\\ {
\\ .entry():
\\ branch .merge(%condition)
\\ .merge(%value: u32):
\\ return
\\ }
\\}
);
}
test "Validator: check terminator operand and return types" {
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ conditional_branch %one, .left(), .right()
\\ .left():
\\ return
\\ .right():
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongReturnType,
\\shader compute @main
\\{
\\ fn @main() -> u32
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongReturnType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ return %one
\\ }
\\}
);
try expectValidationError(Error.WrongReturnType,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ discard
\\ }
\\}
);
}
test "Validator: check unary, binary, compare, and select types" {
try expectValidationError(Error.WrongResultType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: bool = bitwise_not %one
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ %float: constant f32 = 1.0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = integer_add %one, %float
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = cmp_equal %one, %one
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = select %one, %one, %one
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %condition: constant bool = true
\\ %one: constant u32 = 1
\\ %float: constant f32 = 1.0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = select %condition, %one, %float
\\ return
\\ }
\\}
);
}
test "Validator: check composite operations" {
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: vec2[u32] = composite_construct %one
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %vector: vec2[u32] = composite_construct %one, %one
\\ %result: bool = composite_extract %vector[0]
\\ return
\\ }
\\}
);
}
test "Validator: check interface direction and value types" {
try expectValidationError(Error.WrongInterfaceDirection,
\\shader vertex @main
\\{
\\ @output: u32 = output[location(0), component(0), index(0)]
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %value: u32 = load_interface @output
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongInterfaceDirection,
\\shader vertex @main
\\{
\\ @input: u32 = input[location(0), component(0), index(0)]
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ store_interface @input, %one
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader vertex @main
\\{
\\ @output: u32 = output[location(0), component(0), index(0)]
\\ %value: constant f32 = 1.0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ store_interface @output, %value
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultPresence,
\\shader vertex @main
\\{
\\ @output: u32 = output[location(0), component(0), index(0)]
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: bool = store_interface @output, %one
\\ return
\\ }
\\}
);
}
test "Validator: check buffer resources, offsets, and value types" {
try expectValidationError(Error.WrongResourceKind,
\\shader compute @main
\\{
\\ @uniforms: u32 = uniform_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %value: u32 = load_buffer @uniforms, %offset
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResourceKind,
\\shader compute @main
\\{
\\ @uniforms: u32 = uniform_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ %value: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ store_buffer @uniforms, %offset, %value
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ @storage: u32 = storage_buffer[set(0), binding(0)]
\\ %offset: constant i32 = 0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %value: u32 = load_buffer @storage, %offset
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultType,
\\shader compute @main
\\{
\\ @storage: struct[u32, f32] = storage_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %value: struct[u32, f32] = load_buffer @storage, %offset
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ @storage: struct[u32, f32] = storage_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ %value: constant ptr[private, u32] = null
\\ fn @main() -> void
\\ {
\\ .entry():
\\ store_buffer @storage, %offset, %value
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultPresence,
\\shader compute @main
\\{
\\ @storage: struct[u32, f32] = storage_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ fn @main() -> void
\\ {
\\ .entry():
\\ load_buffer @storage, %offset
\\ return
\\ }
\\}
);
try expectValidationError(Error.WrongResultPresence,
\\shader compute @main
\\{
\\ @storage: u32 = storage_buffer[set(0), binding(0)]
\\ %offset: constant u32 = 0
\\ %value: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = store_buffer @storage, %offset, %value
\\ return
\\ }
\\}
);
}
test "Validator: check function calls" {
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = call @identity()
\\ return
\\ }
\\ fn @identity(%value: u32) -> u32
\\ {
\\ .entry():
\\ return %value
\\ }
\\}
);
try expectValidationError(Error.WrongOperandType,
\\shader compute @main
\\{
\\ %condition: constant bool = true
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = call @identity(%condition)
\\ return
\\ }
\\ fn @identity(%value: u32) -> u32
\\ {
\\ .entry():
\\ return %value
\\ }
\\}
);
try expectValidationError(Error.WrongResultType,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: f32 = call @identity(%one)
\\ return
\\ }
\\ fn @identity(%value: u32) -> u32
\\ {
\\ .entry():
\\ return %value
\\ }
\\}
);
try expectValidationError(Error.WrongResultPresence,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %result: u32 = call @helper()
\\ return
\\ }
\\ fn @helper() -> void
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
}
test "Validator: reject cross-function value references" {
try expectValidationError(Error.CrossFunctionReference,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %main_value: u32 = integer_add %one, %one
\\ return
\\ }
\\ fn @helper() -> void
\\ {
\\ .entry():
\\ %result: u32 = integer_add %main_value, %one
\\ return
\\ }
\\}
);
}
test "Validator: reject an SSA definition that does not dominate its use" {
try expectValidationError(Error.DefinitionDoesNotDominateUse,
\\shader compute @main
\\{
\\ %condition: constant bool = true
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ conditional_branch %condition, .left(), .right()
\\ .left():
\\ %left_value: u32 = integer_add %one, %one
\\ branch .merge()
\\ .right():
\\ branch .merge()
\\ .merge():
\\ %result: u32 = integer_multiply %left_value, %one
\\ return
\\ }
\\}
);
}
test "Validator: reject a same-block use before its definition" {
const parser = @import("../parser/parser.zig");
var module = try parser.parseString(std.testing.allocator,
\\shader compute @main
\\{
\\ %one: constant u32 = 1
\\ fn @main() -> void
\\ {
\\ .entry():
\\ %first: u32 = integer_add %one, %one
\\ %second: u32 = integer_multiply %first, %one
\\ return
\\ }
\\}
);
defer module.deinit();
const function = module.functions.get(module.entry_point.?).?;
const block = module.blocks.get(function.entry_block.?).?;
const first_instruction = block.instructions.items[0];
const second_instruction = block.instructions.items[1];
const later_result = module.instructions.get(second_instruction).?.result.?;
module.instructions.getMut(first_instruction).?.operation.binary.lhs = later_result;
try std.testing.expectError(Error.DefinitionDoesNotDominateUse, validate(&module));
}
test "Validator: reject a structured-control target in another function" {
const parser = @import("../parser/parser.zig");
var module = try parser.parseString(std.testing.allocator,
\\shader compute @main
\\{
\\ fn @main() -> void
\\ {
\\ .entry():
\\ return
\\ }
\\ fn @helper() -> void
\\ {
\\ .entry():
\\ return
\\ }
\\}
);
defer module.deinit();
const main = module.functions.get(module.entry_point.?).?;
const helper = module.functions.get(ids.FunctionId.fromIndex(1)).?;
module.blocks.getMut(main.entry_block.?).?.structured_control = .{
.selection = .{ .merge_block = helper.entry_block.? },
};
try std.testing.expectError(Error.InvalidStructuredControl, validate(&module));
}