1230 lines
41 KiB
Zig
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));
|
|
}
|