diff --git a/src/intel/compiler/device.zig b/src/intel/compiler/device.zig new file mode 100644 index 0000000..2129ea0 --- /dev/null +++ b/src/intel/compiler/device.zig @@ -0,0 +1,56 @@ +pub const Generation = enum { + gen9, + gen10, + gen11, +}; + +pub const Platform = enum { + skylake, + broxton, + kabylake, + gemini_lake, + coffee_lake, + whiskey_lake, + comet_lake, + ice_lake, + elkhart_lake, + jasper_lake, +}; + +pub const DeviceInfo = struct { + generation: Generation, + platform: Platform, + pci_device_id: u16, + + grf_count: u16, + grf_size_bytes: u16 = 32, + + supports_int64: bool = false, + supports_float64: bool = false, + supports_half_float: bool = false, + supports_simd16: bool = false, + supports_simd32: bool = false, + + pub fn supportsDispatch(self: DeviceInfo, width: DispatchWidth) bool { + return switch (width) { + .simd8 => true, + .simd16 => self.supports_simd16, + .simd32 => self.supports_simd32, + }; + } +}; + +pub const DispatchWidth = enum(u8) { + simd8 = 8, + simd16 = 16, + simd32 = 32, +}; + +pub const ExecutionSize = enum(u8) { + simd1 = 1, + simd2 = 2, + simd4 = 4, + simd8 = 8, + simd16 = 16, + simd32 = 32, +}; diff --git a/src/intel/compiler/id.zig b/src/intel/compiler/id.zig new file mode 100644 index 0000000..6b86474 --- /dev/null +++ b/src/intel/compiler/id.zig @@ -0,0 +1,16 @@ +const shared_ids = @import("shader_ir").ir.id; + +pub const FunctionTag = opaque {}; +pub const BlockTag = opaque {}; +pub const InstructionTag = opaque {}; +pub const VirtualRegisterTag = opaque {}; +pub const VirtualFlagTag = opaque {}; + +pub const FunctionId = shared_ids.Id(FunctionTag); +pub const BlockId = shared_ids.Id(BlockTag); +pub const InstructionId = shared_ids.Id(InstructionTag); +pub const VirtualRegisterId = shared_ids.Id(VirtualRegisterTag); +pub const VirtualFlagId = shared_ids.Id(VirtualFlagTag); + +pub const Id = shared_ids.Id; +pub const Store = shared_ids.Store; diff --git a/src/intel/compiler/instruction.zig b/src/intel/compiler/instruction.zig new file mode 100644 index 0000000..03845ec --- /dev/null +++ b/src/intel/compiler/instruction.zig @@ -0,0 +1,147 @@ +const std = @import("std"); +const device = @import("device.zig"); +const ids = @import("id.zig"); +const operand = @import("operand.zig"); + +pub const Builtin = enum { + position, + vertex_index, + instance_index, +}; + +pub const InterfaceSemantic = union(enum) { + location: struct { + location: u32, + component: u8 = 0, + }, + builtin: struct { + builtin: Builtin, + component: u8 = 0, + }, +}; + +pub const LoadInput = struct { + destination: operand.Destination, + semantic: InterfaceSemantic, +}; + +pub const StoreOutput = struct { + semantic: InterfaceSemantic, + source: operand.Source, +}; + +pub const Move = struct { + destination: operand.Destination, + source: operand.Source, +}; + +pub const BinaryOpcode = enum { + add, + multiply, + bitwise_and, + bitwise_or, + bitwise_xor, + shift_left, + shift_right, +}; + +pub const Binary = struct { + opcode: BinaryOpcode, + destination: operand.Destination, + lhs: operand.Source, + rhs: operand.Source, +}; + +pub const CompareOpcode = enum { + equal, + not_equal, + less_than, + less_or_equal, + greater_than, + greater_or_equal, +}; + +pub const Compare = struct { + opcode: CompareOpcode, + destination: operand.FlagRef, + lhs: operand.Source, + rhs: operand.Source, +}; + +pub const ChannelMask = packed struct(u4) { + x: bool = true, + y: bool = true, + z: bool = true, + w: bool = true, +}; + +pub const UrbWrite = struct { + offset: u16, + channels: ChannelMask = .{}, + end_of_thread: bool = false, +}; + +pub const Message = union(enum) { + urb_write: UrbWrite, +}; + +pub const Send = struct { + message: Message, + payload: operand.RegisterSpan, + response: ?operand.RegisterSpan = null, +}; + +pub const Call = struct { + function: ids.FunctionId, + destination: ?operand.Destination = null, + arguments: []const operand.Source, +}; + +pub const Operation = union(enum) { + load_input: LoadInput, + store_output: StoreOutput, + move: Move, + binary: Binary, + compare: Compare, + call: Call, + send: Send, +}; + +pub const Instruction = struct { + parent_block: ids.BlockId, + execution_size: device.ExecutionSize, + predicate: ?operand.Predicate = null, + operation: Operation, +}; + +pub const Terminator = union(enum) { + jump: ids.BlockId, + conditional_branch: struct { + predicate: operand.Predicate, + true_block: ids.BlockId, + false_block: ids.BlockId, + }, + return_void, + return_value: operand.Source, + end_thread, + @"unreachable", +}; + +pub const StructuredControl = union(enum) { + none, + selection: struct { + merge_block: ids.BlockId, + }, + loop: struct { + merge_block: ids.BlockId, + continue_block: ids.BlockId, + }, +}; + +pub const Block = struct { + parent_function: ids.FunctionId, + instructions: std.ArrayList(ids.InstructionId) = .empty, + terminator: ?Terminator = null, + structured_control: StructuredControl = .none, + name: ?[]const u8 = null, +}; diff --git a/src/intel/compiler/lower.zig b/src/intel/compiler/lower.zig new file mode 100644 index 0000000..c8f1dfb --- /dev/null +++ b/src/intel/compiler/lower.zig @@ -0,0 +1,74 @@ +const std = @import("std"); +const shader_ir = @import("shader_ir").ir; +const device = @import("device.zig"); +const program_ir = @import("program.zig"); + +pub const Options = struct { + dispatch_width: device.DispatchWidth = .simd8, +}; + +pub const Error = std.mem.Allocator.Error || error{ + MissingEntryPoint, + InvalidEntryPoint, + UnsupportedGeneration, + UnsupportedStage, + UnsupportedDispatchWidth, + UnsupportedType, + UnsupportedOperation, + UnsupportedTerminator, + LoweringNotImplemented, +}; + +pub const Lowerer = struct { + allocator: std.mem.Allocator, + module: *const shader_ir.module.Module, + device_info: device.DeviceInfo, + options: Options, + + pub fn init( + allocator: std.mem.Allocator, + module: *const shader_ir.module.Module, + device_info: device.DeviceInfo, + options: Options, + ) Lowerer { + return .{ + .allocator = allocator, + .module = module, + .device_info = device_info, + .options = options, + }; + } + + pub fn lower(self: *Lowerer) Error!program_ir.Program { + var program = program_ir.Program.init(self.allocator, self.module.stage, self.device_info, self.options.dispatch_width); + errdefer program.deinit(); + + const entry_point = self.source.entryPoint(self.module.stage); + if (entry_point) |ep| { + program.entry_block = try program.addBlock(ep.name); + _ = program.appendInstruction(program.entry_block orelse return error.InvalidBlock, self.options.dispatch_width, null, .{ .name = ep.name }); + } + + const blocks = self.source.blocks(); + for (blocks) |block| { + const block_id = try program.addBlock(block.name); + for (block.instructions) |inst| { + _ = program.appendInstruction(block_id, self.options.dispatch_width, null, .{ .name = inst.name }); + } + _ = program.setTerminator(block_id, .{ .name = block.terminator.name }); + } + + return program; + } +}; + +/// Convenience entry point for callers that do not need to retain a lowerer. +pub inline fn lower( + allocator: std.mem.Allocator, + module: *const shader_ir.module.Module, + device_info: device.DeviceInfo, + options: Options, +) Error!program_ir.Program { + var lowerer = Lowerer.init(allocator, module, device_info, options); + return lowerer.lower(); +} diff --git a/src/intel/compiler/operand.zig b/src/intel/compiler/operand.zig new file mode 100644 index 0000000..21bc0e6 --- /dev/null +++ b/src/intel/compiler/operand.zig @@ -0,0 +1,148 @@ +const device = @import("device.zig"); +const ids = @import("id.zig"); + +pub const DataType = enum { + u8, + i8, + u16, + i16, + f16, + u32, + i32, + f32, + u64, + i64, + f64, + + pub fn sizeBytes(self: DataType) u8 { + return switch (self) { + .u8, .i8 => 1, + .u16, .i16, .f16 => 2, + .u32, .i32, .f32 => 4, + .u64, .i64, .f64 => 8, + }; + } + + pub fn isInitialTargetType(self: DataType) bool { + return switch (self) { + .u32, .i32, .f32 => true, + else => false, + }; + } +}; + +pub const RegisterClass = enum { + uniform, + varying, + payload, + response, + temporary, +}; + +pub const VirtualRegister = struct { + size_bytes: u32, + alignment_bytes: u16, + element_type: DataType, + lane_count: u8, + class: RegisterClass, + spillable: bool = true, + name: ?[]const u8 = null, +}; + +pub const VirtualFlag = struct { + name: ?[]const u8 = null, +}; + +pub const PhysicalGrf = struct { + number: u16, + byte_offset: u8 = 0, +}; + +pub const PhysicalFlag = struct { + register: u8 = 0, + subregister: u8 = 0, +}; + +pub const ArchitectureRegister = union(enum) { + flag: u8, + address: u8, + accumulator: u8, + notification: u8, + instruction_pointer, +}; + +pub const Immediate = union(enum) { + u32: u32, + i32: i32, + f32: f32, +}; + +pub const RegisterRef = union(enum) { + virtual: ids.VirtualRegisterId, + physical_grf: PhysicalGrf, + architecture: ArchitectureRegister, + immediate: Immediate, + null, +}; + +pub const FlagRef = union(enum) { + virtual: ids.VirtualFlagId, + physical: PhysicalFlag, +}; + +pub const Predicate = struct { + flag: FlagRef, + inverse: bool = false, +}; + +pub const Region = struct { + byte_offset: u16 = 0, + vertical_stride: u8, + width: u8, + horizontal_stride: u8, + + pub fn scalar() Region { + return .{ + .vertical_stride = 0, + .width = 1, + .horizontal_stride = 0, + }; + } + + pub fn contiguous(execution_size: device.ExecutionSize) Region { + const width: u8 = @intFromEnum(execution_size); + return .{ + .vertical_stride = width, + .width = width, + .horizontal_stride = 1, + }; + } + + pub fn broadcast() Region { + return scalar(); + } +}; + +pub const DestinationRegion = struct { + byte_offset: u16 = 0, + horizontal_stride: u8 = 1, +}; + +pub const Source = struct { + register: RegisterRef, + type: DataType, + region: Region, + negate: bool = false, + absolute: bool = false, +}; + +pub const Destination = struct { + register: RegisterRef, + type: DataType, + region: DestinationRegion = .{}, +}; + +pub const RegisterSpan = struct { + base: RegisterRef, + register_count: u8, +}; diff --git a/src/intel/compiler/printer.zig b/src/intel/compiler/printer.zig new file mode 100644 index 0000000..28c9738 --- /dev/null +++ b/src/intel/compiler/printer.zig @@ -0,0 +1,364 @@ +const std = @import("std"); +const ids = @import("id.zig"); +const inst_ir = @import("instruction.zig"); +const operand = @import("operand.zig"); +const program_ir = @import("program.zig"); + +const indent = " "; + +pub fn write(program: *const program_ir.Program, writer: *std.Io.Writer) std.Io.Writer.Error!void { + try writer.writeAll("; Flint program:\n"); + try writer.print("; .stage: {t}\n", .{program.stage}); + try writer.print("; .generation: {t}\n", .{program.device_info.generation}); + try writer.print("; .platform: {t}\n", .{program.device_info.platform}); + try writer.print("; .dispatch_width: {t}\n", .{program.dispatch_width}); + if (program.entry_function) |entry| { + try writer.writeAll("; .entry: "); + try writeFunctionRef(program, writer, entry); + try writer.writeByte('\n'); + } + + try writer.writeByte('\n'); + + for (program.virtual_registers.entries.items, 0..) |entry, index| { + const register = entry orelse continue; + try writeVirtualRegisterRef(program, writer, ids.VirtualRegisterId.fromIndex(index)); + try writer.print(": vgrf {t}[{d}] = class({t}), size({d}), alignment({d}), spillable({})\n", .{ + register.element_type, + register.lane_count, + register.class, + register.size_bytes, + register.alignment_bytes, + register.spillable, + }); + } + + for (program.virtual_flags.entries.items, 0..) |entry, index| { + _ = entry orelse continue; + try writeVirtualFlagRef(program, writer, ids.VirtualFlagId.fromIndex(index)); + try writer.writeAll(": vflag\n"); + } + + for (program.functions.entries.items, 0..) |entry, function_index| { + const function = entry orelse continue; + + try writer.writeAll("\nfn "); + try writeFunctionRef(program, writer, ids.FunctionId.fromIndex(function_index)); + try writer.writeByte('('); + for (function.parameters.items, 0..) |parameter, index| { + if (index != 0) + try writer.writeAll(", "); + try writeVirtualRegisterRef(program, writer, parameter); + try writer.writeAll(": "); + if (program.virtual_registers.get(parameter)) |register| + try writer.print("{t}", .{register.element_type}) + else + try writer.print("", .{parameter.index()}); + } + try writer.writeAll(") -> "); + if (function.return_type) |return_type| + try writer.print("{t}", .{return_type}) + else + try writer.writeAll("void"); + try writer.writeAll("\n{\n"); + + for (function.blocks.items) |block_id| { + const block = program.blocks.get(block_id) orelse continue; + + try writer.writeAll(indent); + try writeBlockRef(program, writer, block_id); + try writer.writeAll(":\n"); + + switch (block.structured_control) { + .none => {}, + .selection => |selection| { + try writer.writeAll(indent ** 2 ++ "structured_selection "); + try writeBlockRef(program, writer, selection.merge_block); + try writer.writeByte('\n'); + }, + .loop => |loop| { + try writer.writeAll(indent ** 2 ++ "structured_loop merge("); + try writeBlockRef(program, writer, loop.merge_block); + try writer.writeAll("), continue("); + try writeBlockRef(program, writer, loop.continue_block); + try writer.writeAll(")\n"); + }, + } + + for (block.instructions.items) |instruction_id| { + const instruction = program.instructions.get(instruction_id) orelse continue; + try writer.writeAll(indent ** 2); + try writeInstruction(program, writer, instruction.*); + try writer.writeByte('\n'); + } + + if (block.terminator) |terminator| { + try writer.writeAll(indent ** 2); + try writeTerminator(program, writer, terminator); + try writer.writeAll("\n\n"); + } else { + try writer.writeAll(indent ** 2 ++ "\n\n"); + } + } + + try writer.writeAll("}\n"); + } +} + +pub fn allocPrint(allocator: std.mem.Allocator, program: *const program_ir.Program) ![]u8 { + var output: std.Io.Writer.Allocating = .init(allocator); + defer output.deinit(); + try write(program, &output.writer); + return output.toOwnedSlice(); +} + +fn writeInstruction(program: *const program_ir.Program, writer: *std.Io.Writer, instruction: inst_ir.Instruction) !void { + try writer.print("[simd{d}] ", .{@intFromEnum(instruction.execution_size)}); + if (instruction.predicate) |predicate| { + try writePredicate(program, writer, predicate); + try writer.writeByte(' '); + } + try writeOperation(program, writer, instruction.operation); +} + +fn writeOperation(program: *const program_ir.Program, writer: *std.Io.Writer, operation: inst_ir.Operation) !void { + switch (operation) { + .load_input => |op| { + try writeDestination(program, writer, op.destination); + try writer.writeAll(" = load_input "); + try writeInterfaceSemantic(writer, op.semantic); + }, + .store_output => |op| { + try writer.writeAll("store_output "); + try writeInterfaceSemantic(writer, op.semantic); + try writer.writeAll(", "); + try writeSource(program, writer, op.source); + }, + .move => |op| { + try writeDestination(program, writer, op.destination); + try writer.writeAll(" = move "); + try writeSource(program, writer, op.source); + }, + .binary => |op| { + try writeDestination(program, writer, op.destination); + try writer.print(" = {t} ", .{op.opcode}); + try writeSource(program, writer, op.lhs); + try writer.writeAll(", "); + try writeSource(program, writer, op.rhs); + }, + .compare => |op| { + try writeFlagRef(program, writer, op.destination); + try writer.print(" = cmp_{t} ", .{op.opcode}); + try writeSource(program, writer, op.lhs); + try writer.writeAll(", "); + try writeSource(program, writer, op.rhs); + }, + .call => |op| { + if (op.destination) |destination| { + try writeDestination(program, writer, destination); + try writer.writeAll(" = "); + } + try writer.writeAll("call "); + try writeFunctionRef(program, writer, op.function); + try writer.writeByte('('); + for (op.arguments, 0..) |argument, index| { + if (index != 0) + try writer.writeAll(", "); + try writeSource(program, writer, argument); + } + try writer.writeByte(')'); + }, + .send => |op| { + if (op.response) |response| { + try writeRegisterSpan(program, writer, response); + try writer.writeAll(" = "); + } + try writer.writeAll("send "); + try writeMessage(writer, op.message); + try writer.writeAll(", payload("); + try writeRegisterSpan(program, writer, op.payload); + try writer.writeByte(')'); + }, + } +} + +fn writeTerminator(program: *const program_ir.Program, writer: *std.Io.Writer, terminator: inst_ir.Terminator) !void { + switch (terminator) { + .jump => |target| { + try writer.writeAll("jump "); + try writeBlockRef(program, writer, target); + }, + .conditional_branch => |branch| { + try writer.writeAll("conditional_branch "); + try writePredicate(program, writer, branch.predicate); + try writer.writeAll(", "); + try writeBlockRef(program, writer, branch.true_block); + try writer.writeAll(", "); + try writeBlockRef(program, writer, branch.false_block); + }, + .return_void => try writer.writeAll("return"), + .return_value => |value| { + try writer.writeAll("return "); + try writeSource(program, writer, value); + }, + .end_thread => try writer.writeAll("end_thread"), + .@"unreachable" => try writer.writeAll("unreachable"), + } +} + +fn writeSource(program: *const program_ir.Program, writer: *std.Io.Writer, source: operand.Source) !void { + if (source.negate) + try writer.writeByte('-'); + if (source.absolute) + try writer.writeAll("abs("); + + switch (source.register) { + .immediate => |immediate| try writeImmediate(writer, immediate), + else => { + try writeRegisterAtOffset(program, writer, source.register, source.region.byte_offset); + try writer.print("<{d};{d},{d}>", .{ + source.region.vertical_stride, + source.region.width, + source.region.horizontal_stride, + }); + }, + } + try writer.print(":{t}", .{source.type}); + + if (source.absolute) + try writer.writeByte(')'); +} + +fn writeDestination(program: *const program_ir.Program, writer: *std.Io.Writer, destination: operand.Destination) !void { + try writeRegisterAtOffset(program, writer, destination.register, destination.region.byte_offset); + try writer.print("<{d}>:{t}", .{ destination.region.horizontal_stride, destination.type }); +} + +fn writeRegisterAtOffset( + program: *const program_ir.Program, + writer: *std.Io.Writer, + register: operand.RegisterRef, + byte_offset: u16, +) !void { + switch (register) { + .virtual => |virtual| { + try writeVirtualRegisterRef(program, writer, virtual); + try writer.print(".{d}", .{byte_offset}); + }, + .physical_grf => |physical| try writer.print("r{d}.{d}", .{ + physical.number, + @as(u16, physical.byte_offset) + byte_offset, + }), + .architecture => |architecture| { + try writeArchitectureRegister(writer, architecture); + try writer.print(".{d}", .{byte_offset}); + }, + .immediate => |immediate| try writeImmediate(writer, immediate), + .null => try writer.writeAll("null"), + } +} + +fn writeArchitectureRegister(writer: *std.Io.Writer, register: operand.ArchitectureRegister) !void { + switch (register) { + .flag => |index| try writer.print("f{d}", .{index}), + .address => |index| try writer.print("a{d}", .{index}), + .accumulator => |index| try writer.print("acc{d}", .{index}), + .notification => |index| try writer.print("n{d}", .{index}), + .instruction_pointer => try writer.writeAll("ip"), + } +} + +fn writeImmediate(writer: *std.Io.Writer, immediate: operand.Immediate) !void { + switch (immediate) { + .u32 => |value| try writer.print("{d}", .{value}), + .i32 => |value| try writer.print("{d}", .{value}), + .f32 => |value| try writer.print("{d}", .{value}), + } +} + +fn writePredicate(program: *const program_ir.Program, writer: *std.Io.Writer, predicate: operand.Predicate) !void { + try writer.writeAll(if (predicate.inverse) "(-" else "(+"); + try writeFlagRef(program, writer, predicate.flag); + try writer.writeByte(')'); +} + +fn writeFlagRef(program: *const program_ir.Program, writer: *std.Io.Writer, flag: operand.FlagRef) !void { + switch (flag) { + .virtual => |virtual| try writeVirtualFlagRef(program, writer, virtual), + .physical => |physical| try writer.print("f{d}.{d}", .{ physical.register, physical.subregister }), + } +} + +fn writeRegisterSpan(program: *const program_ir.Program, writer: *std.Io.Writer, span: operand.RegisterSpan) !void { + try writeRegisterAtOffset(program, writer, span.base, 0); + try writer.print("[{d}]", .{span.register_count}); +} + +fn writeInterfaceSemantic(writer: *std.Io.Writer, semantic: inst_ir.InterfaceSemantic) !void { + switch (semantic) { + .location => |location| try writer.print("location({d}), component({d})", .{ location.location, location.component }), + .builtin => |builtin| try writer.print("builtin({t}), component({d})", .{ builtin.builtin, builtin.component }), + } +} + +fn writeMessage(writer: *std.Io.Writer, message: inst_ir.Message) !void { + switch (message) { + .urb_write => |urb| { + try writer.print("urb_write[offset({d}), channels(", .{urb.offset}); + try writeChannelMask(writer, urb.channels); + try writer.writeByte(')'); + if (urb.end_of_thread) + try writer.writeAll(", end_of_thread"); + try writer.writeByte(']'); + }, + } +} + +fn writeChannelMask(writer: *std.Io.Writer, mask: inst_ir.ChannelMask) !void { + if (mask.x) try writer.writeByte('x'); + if (mask.y) try writer.writeByte('y'); + if (mask.z) try writer.writeByte('z'); + if (mask.w) try writer.writeByte('w'); +} + +fn writeVirtualRegisterRef(program: *const program_ir.Program, writer: *std.Io.Writer, register_id: ids.VirtualRegisterId) !void { + const register = program.virtual_registers.get(register_id); + try writeNamedRef(writer, if (register) |value| value.name else null, "v", register_id.index(), '%'); +} + +fn writeVirtualFlagRef(program: *const program_ir.Program, writer: *std.Io.Writer, flag_id: ids.VirtualFlagId) !void { + const flag = program.virtual_flags.get(flag_id); + try writeNamedRef(writer, if (flag) |value| value.name else null, "f", flag_id.index(), '%'); +} + +fn writeBlockRef(program: *const program_ir.Program, writer: *std.Io.Writer, block_id: ids.BlockId) !void { + const block = program.blocks.get(block_id); + try writeNamedRef(writer, if (block) |value| value.name else null, "b", block_id.index(), '.'); +} + +fn writeFunctionRef(program: *const program_ir.Program, writer: *std.Io.Writer, function_id: ids.FunctionId) !void { + const function = program.functions.get(function_id); + try writeNamedRef(writer, if (function) |value| value.name else null, "fn", function_id.index(), '@'); +} + +fn writeNamedRef(writer: *std.Io.Writer, name: ?[]const u8, fallback: []const u8, index: usize, prefix: u8) !void { + try writer.writeByte(prefix); + if (name) |text| { + if (isValidName(text)) { + try writer.writeAll(text); + return; + } + } + try writer.print("{s}{d}", .{ fallback, index }); +} + +fn isValidName(name: []const u8) bool { + if (name.len == 0 or (!std.ascii.isAlphabetic(name[0]) and name[0] != '_')) + return false; + + for (name[1..]) |byte| { + if (!std.ascii.isAlphanumeric(byte) and byte != '_') + return false; + } + return true; +} diff --git a/src/intel/compiler/program.zig b/src/intel/compiler/program.zig new file mode 100644 index 0000000..147115d --- /dev/null +++ b/src/intel/compiler/program.zig @@ -0,0 +1,182 @@ +const std = @import("std"); +const shared_ir = @import("shader_ir").ir.module; +const device = @import("device.zig"); +const ids = @import("id.zig"); +const instructions = @import("instruction.zig"); +const operand = @import("operand.zig"); + +pub const Stage = shared_ir.Stage; + +pub const Properties = packed struct { + instructions_selected: bool = false, + block_parameters_lowered: bool = false, + calls_lowered: bool = false, + + stage_io_lowered: bool = false, + resources_lowered: bool = false, + messages_lowered: bool = false, + control_flow_lowered: bool = false, + + regions_legalized: bool = false, + types_legalized: bool = false, + + registers_allocated: bool = false, + flags_allocated: bool = false, + branches_resolved: bool = false, + + _padding: u20 = 0, +}; + +pub const VertexPayload = struct { + first_attribute_grf: operand.PhysicalGrf, + attribute_grf_count: u16, +}; + +pub const PayloadLayout = struct { + header_grf: ?operand.PhysicalGrf = null, + vertex: ?VertexPayload = null, +}; + +pub const ProgramData = struct { + payload_grf_count: u16 = 0, + total_grf_count: u16 = 0, + scratch_size_bytes: u32 = 0, +}; + +pub const Function = struct { + return_type: ?operand.DataType, + parameters: std.ArrayList(ids.VirtualRegisterId) = .empty, + blocks: std.ArrayList(ids.BlockId) = .empty, + entry_block: ?ids.BlockId = null, + name: ?[]const u8 = null, +}; + +pub const FunctionStore = ids.Store(ids.FunctionId, Function); +pub const BlockStore = ids.Store(ids.BlockId, instructions.Block); +pub const InstructionStore = ids.Store(ids.InstructionId, instructions.Instruction); +pub const VirtualRegisterStore = ids.Store(ids.VirtualRegisterId, operand.VirtualRegister); +pub const VirtualFlagStore = ids.Store(ids.VirtualFlagId, operand.VirtualFlag); + +pub const Program = struct { + arena: std.heap.ArenaAllocator, + + stage: Stage, + device_info: device.DeviceInfo, + dispatch_width: device.DispatchWidth, + + entry_function: ?ids.FunctionId = null, + + functions: FunctionStore = .{}, + blocks: BlockStore = .{}, + instructions: InstructionStore = .{}, + virtual_registers: VirtualRegisterStore = .{}, + virtual_flags: VirtualFlagStore = .{}, + + payload: PayloadLayout = .{}, + program_data: ProgramData = .{}, + properties: Properties = .{}, + + pub fn init( + backing_allocator: std.mem.Allocator, + stage: Stage, + device_info: device.DeviceInfo, + dispatch_width: device.DispatchWidth, + ) Program { + return .{ + .arena = std.heap.ArenaAllocator.init(backing_allocator), + .stage = stage, + .device_info = device_info, + .dispatch_width = dispatch_width, + }; + } + + pub fn deinit(self: *Program) void { + self.arena.deinit(); + self.* = undefined; + } + + pub fn allocator(self: *Program) std.mem.Allocator { + return self.arena.allocator(); + } + + pub fn addFunction(self: *Program, return_type: ?operand.DataType, name: ?[]const u8) !ids.FunctionId { + const owned_name = if (name) |value| try self.allocator().dupe(u8, value) else null; + return self.functions.add(self.allocator(), .{ + .return_type = return_type, + .name = owned_name, + }); + } + + pub fn setEntryFunction(self: *Program, function_id: ids.FunctionId) !void { + if (!self.functions.isLive(function_id)) + return error.InvalidFunction; + self.entry_function = function_id; + } + + pub fn addFunctionParameter(self: *Program, function_id: ids.FunctionId, register_id: ids.VirtualRegisterId) !void { + const function = self.functions.getMut(function_id) orelse return error.InvalidFunction; + if (!self.virtual_registers.isLive(register_id)) + return error.InvalidVirtualRegister; + try function.parameters.append(self.allocator(), register_id); + } + + pub fn addVirtualRegister(self: *Program, register: operand.VirtualRegister) !ids.VirtualRegisterId { + var owned = register; + if (register.name) |name| + owned.name = try self.allocator().dupe(u8, name); + return self.virtual_registers.add(self.allocator(), owned); + } + + pub fn addVirtualFlag(self: *Program, flag: operand.VirtualFlag) !ids.VirtualFlagId { + var owned = flag; + if (flag.name) |name| + owned.name = try self.allocator().dupe(u8, name); + return self.virtual_flags.add(self.allocator(), owned); + } + + pub fn addBlock(self: *Program, function_id: ids.FunctionId, name: ?[]const u8) !ids.BlockId { + const function = self.functions.getMut(function_id) orelse return error.InvalidFunction; + const owned_name = if (name) |value| try self.allocator().dupe(u8, value) else null; + const block_id = try self.blocks.add(self.allocator(), .{ + .parent_function = function_id, + .name = owned_name, + }); + errdefer _ = self.blocks.remove(block_id); + + try function.blocks.append(self.allocator(), block_id); + if (function.entry_block == null) + function.entry_block = block_id; + return block_id; + } + + pub fn appendInstruction( + self: *Program, + block_id: ids.BlockId, + execution_size: device.ExecutionSize, + predicate: ?operand.Predicate, + operation: instructions.Operation, + ) !ids.InstructionId { + const block = self.blocks.getMut(block_id) orelse return error.InvalidBlock; + var owned_operation = operation; + switch (owned_operation) { + .call => |*call| call.arguments = try self.allocator().dupe(operand.Source, call.arguments), + else => {}, + } + + const instruction_id = try self.instructions.add(self.allocator(), .{ + .parent_block = block_id, + .execution_size = execution_size, + .predicate = predicate, + .operation = owned_operation, + }); + try block.instructions.append(self.allocator(), instruction_id); + return instruction_id; + } + + pub fn setTerminator(self: *Program, block_id: ids.BlockId, terminator: instructions.Terminator) !void { + const block = self.blocks.getMut(block_id) orelse return error.InvalidBlock; + if (block.terminator != null) + return error.TerminatorAlreadySet; + block.terminator = terminator; + } +}; diff --git a/src/intel/compiler/root.zig b/src/intel/compiler/root.zig new file mode 100644 index 0000000..c8db6c1 --- /dev/null +++ b/src/intel/compiler/root.zig @@ -0,0 +1,266 @@ +//! Backend-specific shader IR for Intel Gen hardware. +//! This is the mutable, non-SSA layer between the shared shader IR and machine encoding. + +pub const device = @import("device.zig"); +pub const id = @import("id.zig"); +pub const instruction = @import("instruction.zig"); +pub const lower = @import("lower.zig"); +pub const operand = @import("operand.zig"); +pub const printer = @import("printer.zig"); +pub const program = @import("program.zig"); +pub const validator = @import("validator.zig"); + +pub const Function = program.Function; +pub const FunctionId = id.FunctionId; +pub const Program = program.Program; +pub const Stage = program.Stage; + +const std = @import("std"); + +test "Flint IR foundation" { + // ; Flint program: + // ; .stage: vertex + // ; .generation: gen9 + // ; .platform: skylake + // ; .dispatch_width: simd8 + // ; .entry: @main + // + // %position: vgrf f32[8] = class(varying), size(32), alignment(32), spillable(true) + // %urb_payload: vgrf u32[16] = class(payload), size(64), alignment(32), spillable(false) + // + // fn @main() -> void + // { + // .entry: + // [simd8] %position.0<1>:f32 = load_input location(0), component(0) + // [simd8] %position.0<1>:f32 = multiply %position.0<8;8,1>:f32, 1:f32 + // [simd8] store_output builtin(position), component(0), %position.0<8;8,1>:f32 + // [simd8] send urb_write[offset(0), channels(xyzw), end_of_thread], payload(%urb_payload.0[2]) + // end_thread + // + // } + + const device_info: device.DeviceInfo = .{ + .generation = .gen9, + .platform = .skylake, + .pci_device_id = 0x1912, + .grf_count = 128, + }; + + var shader = Program.init(std.testing.allocator, .vertex, device_info, .simd8); + defer shader.deinit(); + + const main = try shader.addFunction(null, "main"); + try shader.setEntryFunction(main); + + const position = try shader.addVirtualRegister(.{ + .size_bytes = 32, + .alignment_bytes = 32, + .element_type = .f32, + .lane_count = 8, + .class = .varying, + .name = "position", + }); + const urb_payload = try shader.addVirtualRegister(.{ + .size_bytes = 64, + .alignment_bytes = 32, + .element_type = .u32, + .lane_count = 16, + .class = .payload, + .spillable = false, + .name = "urb_payload", + }); + const entry = try shader.addBlock(main, "entry"); + + _ = try shader.appendInstruction(entry, .simd8, null, .{ + .load_input = .{ + .destination = .{ + .register = .{ .virtual = position }, + .type = .f32, + }, + .semantic = .{ + .location = .{ + .location = 0, + }, + }, + }, + }); + _ = try shader.appendInstruction(entry, .simd8, null, .{ + .binary = .{ + .opcode = .multiply, + .destination = .{ + .register = .{ .virtual = position }, + .type = .f32, + }, + .lhs = .{ + .register = .{ .virtual = position }, + .type = .f32, + .region = operand.Region.contiguous(.simd8), + }, + .rhs = .{ + .register = .{ + .immediate = .{ .f32 = 1.0 }, + }, + .type = .f32, + .region = operand.Region.broadcast(), + }, + }, + }); + _ = try shader.appendInstruction(entry, .simd8, null, .{ + .store_output = .{ + .semantic = .{ + .builtin = .{ .builtin = .position }, + }, + .source = .{ + .register = .{ .virtual = position }, + .type = .f32, + .region = operand.Region.contiguous(.simd8), + }, + }, + }); + _ = try shader.appendInstruction(entry, .simd8, null, .{ + .send = .{ + .message = .{ + .urb_write = .{ + .offset = 0, + .end_of_thread = true, + }, + }, + .payload = .{ + .base = .{ .virtual = urb_payload }, + .register_count = 2, + }, + }, + }); + try shader.setTerminator(entry, .end_thread); + + shader.properties.instructions_selected = true; + try validator.validate(&shader); + + try std.testing.expectEqual(main, shader.entry_function.?); + try std.testing.expectEqual(entry, shader.functions.get(main).?.entry_block.?); + try std.testing.expect(shader.properties.instructions_selected); + + const text = try printer.allocPrint(std.testing.allocator, &shader); + defer std.testing.allocator.free(text); + + try std.testing.expect(std.mem.indexOf(u8, text, "Flint program") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "vertex") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "gen9") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "skylake") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "simd8") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "@main") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "%position: vgrf f32[8]") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "[simd8] %position.0<1>:f32 = multiply %position.0<8;8,1>:f32, 1:f32") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "send urb_write[offset(0), channels(xyzw), end_of_thread], payload(%urb_payload.0[2])") != null); +} + +test "Flint IR function calls" { + // ; Flint program: + // ; .stage: vertex + // ; .generation: gen9 + // ; .platform: skylake + // ; .dispatch_width: simd8 + // ; .entry: @main + // + // %value: vgrf u32[8] = class(temporary), size(32), alignment(32), spillable(true) + // %result: vgrf u32[8] = class(temporary), size(32), alignment(32), spillable(true) + // + // fn @main() -> void + // { + // .entry: + // [simd8] %result.0<1>:u32 = call @identity(1:u32) + // end_thread + // + // } + // + // fn @identity(%value: u32) -> u32 + // { + // .entry: + // return %value.0<8;8,1>:u32 + // + // } + + const device_info: device.DeviceInfo = .{ + .generation = .gen9, + .platform = .skylake, + .pci_device_id = 0x1912, + .grf_count = 128, + }; + + var shader = Program.init(std.testing.allocator, .vertex, device_info, .simd8); + defer shader.deinit(); + + const main = try shader.addFunction(null, "main"); + const identity = try shader.addFunction(.u32, "identity"); + try shader.setEntryFunction(main); + + const parameter = try shader.addVirtualRegister(.{ + .size_bytes = 32, + .alignment_bytes = 32, + .element_type = .u32, + .lane_count = 8, + .class = .temporary, + .name = "value", + }); + const result = try shader.addVirtualRegister(.{ + .size_bytes = 32, + .alignment_bytes = 32, + .element_type = .u32, + .lane_count = 8, + .class = .temporary, + .name = "result", + }); + try shader.addFunctionParameter(identity, parameter); + + const main_entry = try shader.addBlock(main, "entry"); + const identity_entry = try shader.addBlock(identity, "entry"); + + const call_id = try shader.appendInstruction(main_entry, .simd8, null, .{ + .call = .{ + .function = identity, + .destination = .{ + .register = .{ .virtual = result }, + .type = .u32, + }, + .arguments = &.{ + .{ + .register = .{ .immediate = .{ .u32 = 1 } }, + .type = .u32, + .region = operand.Region.broadcast(), + }, + }, + }, + }); + try shader.setTerminator(main_entry, .end_thread); + try shader.setTerminator(identity_entry, .{ .return_value = .{ + .register = .{ .virtual = parameter }, + .type = .u32, + .region = operand.Region.contiguous(.simd8), + } }); + + try validator.validate(&shader); + + const call = shader.instructions.get(call_id).?.operation.call; + try std.testing.expectEqual(identity, call.function); + try std.testing.expectEqual(@as(usize, 1), call.arguments.len); + + const text = try printer.allocPrint(std.testing.allocator, &shader); + defer std.testing.allocator.free(text); + + try std.testing.expect(std.mem.indexOf(u8, text, "fn @identity(%value: u32) -> u32") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "[simd8] %result.0<1>:u32 = call @identity(1:u32)") != null); + try std.testing.expect(std.mem.indexOf(u8, text, "return %value.0<8;8,1>:u32") != null); +} + +test "ID stability after removal" { + var store: id.Store(id.VirtualFlagId, operand.VirtualFlag) = .{}; + defer store.entries.deinit(std.testing.allocator); + + const first = try store.add(std.testing.allocator, .{ .name = "first" }); + try std.testing.expect(store.remove(first)); + const second = try store.add(std.testing.allocator, .{ .name = "second" }); + + try std.testing.expect(first != second); + try std.testing.expect(store.get(first) == null); + try std.testing.expectEqualStrings("second", store.get(second).?.name.?); +} diff --git a/src/intel/compiler/validator.zig b/src/intel/compiler/validator.zig new file mode 100644 index 0000000..c7230c7 --- /dev/null +++ b/src/intel/compiler/validator.zig @@ -0,0 +1,282 @@ +const ids = @import("id.zig"); +const instruction = @import("instruction.zig"); +const operand = @import("operand.zig"); +const program_ir = @import("program.zig"); + +pub const Error = error{ + UnsupportedGeneration, + UnsupportedStage, + UnsupportedDispatchWidth, + UnsupportedExecutionSize, + UnsupportedDataType, + MissingEntryFunction, + InvalidFunction, + MissingEntryBlock, + InvalidBlock, + WrongParentFunction, + CrossFunctionBranch, + MissingTerminator, + InvalidInstruction, + InvalidVirtualRegister, + InvalidVirtualFlag, + InvalidPhysicalRegister, + InvalidPhysicalFlag, + InvalidRegisterSize, + InvalidRegisterAlignment, + InvalidLaneCount, + InvalidRegion, + InvalidDestination, + InvalidImmediateType, + InvalidRegisterSpan, + WrongArgumentCount, + WrongArgumentType, + WrongReturnType, + InvalidEntryTerminator, +}; + +pub fn validate(program: *const program_ir.Program) Error!void { + if (program.device_info.generation != .gen9) + return error.UnsupportedGeneration; + if (program.stage != .vertex) + return error.UnsupportedStage; + if (program.dispatch_width != .simd8 or !program.device_info.supportsDispatch(.simd8)) + return error.UnsupportedDispatchWidth; + + const entry_function_id = program.entry_function orelse return error.MissingEntryFunction; + const entry_function = program.functions.get(entry_function_id) orelse return error.InvalidFunction; + const entry_block = entry_function.entry_block orelse return error.MissingEntryBlock; + const entry_block_data = program.blocks.get(entry_block) orelse return error.InvalidBlock; + if (entry_block_data.parent_function != entry_function_id) + return error.WrongParentFunction; + + for (program.functions.entries.items, 0..) |entry, function_index| { + const function = entry orelse continue; + const function_id = ids.FunctionId.fromIndex(function_index); + + if (function.return_type) |return_type| + try validateType(return_type); + const function_entry = function.entry_block orelse return error.MissingEntryBlock; + const function_entry_data = program.blocks.get(function_entry) orelse return error.InvalidBlock; + if (function_entry_data.parent_function != function_id) + return error.WrongParentFunction; + + for (function.parameters.items) |parameter| { + if (!program.virtual_registers.isLive(parameter)) + return error.InvalidVirtualRegister; + } + + for (function.blocks.items) |block_id| { + const block = program.blocks.get(block_id) orelse return error.InvalidBlock; + if (block.parent_function != function_id) + return error.WrongParentFunction; + } + } + + for (program.virtual_registers.entries.items) |entry| { + const register = entry orelse continue; + if (register.size_bytes == 0) + return error.InvalidRegisterSize; + if (register.alignment_bytes == 0 or + (register.alignment_bytes & (register.alignment_bytes - 1)) != 0) + return error.InvalidRegisterAlignment; + if (register.lane_count == 0) + return error.InvalidLaneCount; + try validateType(register.element_type); + } + + for (program.blocks.entries.items, 0..) |entry, block_index| { + const block = entry orelse continue; + if (!program.functions.isLive(block.parent_function) or !functionContainsBlock(program, block.parent_function, ids.BlockId.fromIndex(block_index))) + return error.WrongParentFunction; + if (block.terminator == null) + return error.MissingTerminator; + + const block_id = ids.BlockId.fromIndex(block_index); + for (block.instructions.items) |instruction_id| { + const inst = program.instructions.get(instruction_id) orelse return error.InvalidInstruction; + if (inst.parent_block != block_id) + return error.InvalidInstruction; + try validateInstruction(program, inst.*); + } + + try validateStructuredControl(program, block.parent_function, block.structured_control); + try validateTerminator(program, block.parent_function, block.terminator.?); + } +} + +fn validateInstruction(program: *const program_ir.Program, inst: instruction.Instruction) Error!void { + switch (inst.execution_size) { + .simd1, .simd8 => {}, + else => return error.UnsupportedExecutionSize, + } + + if (inst.predicate) |predicate| + try validateFlag(program, predicate.flag); + + switch (inst.operation) { + .load_input => |op| try validateDestination(program, op.destination), + .store_output => |op| try validateSource(program, op.source), + .move => |op| { + try validateDestination(program, op.destination); + try validateSource(program, op.source); + }, + .binary => |op| { + try validateDestination(program, op.destination); + try validateSource(program, op.lhs); + try validateSource(program, op.rhs); + }, + .compare => |op| { + try validateFlag(program, op.destination); + try validateSource(program, op.lhs); + try validateSource(program, op.rhs); + }, + .call => |op| try validateCall(program, op), + .send => |op| { + try validateSpan(program, op.payload); + if (op.response) |response| + try validateSpan(program, response); + }, + } +} + +fn validateCall(program: *const program_ir.Program, call: instruction.Call) Error!void { + const function = program.functions.get(call.function) orelse return error.InvalidFunction; + if (call.arguments.len != function.parameters.items.len) + return error.WrongArgumentCount; + + for (call.arguments, function.parameters.items) |argument, parameter_id| { + try validateSource(program, argument); + const parameter = program.virtual_registers.get(parameter_id) orelse return error.InvalidVirtualRegister; + if (argument.type != parameter.element_type) + return error.WrongArgumentType; + } + + if (function.return_type) |return_type| { + const destination = call.destination orelse return error.WrongReturnType; + try validateDestination(program, destination); + if (destination.type != return_type) + return error.WrongReturnType; + } else if (call.destination != null) { + return error.WrongReturnType; + } +} + +fn validateType(data_type: operand.DataType) Error!void { + if (!data_type.isInitialTargetType()) + return error.UnsupportedDataType; +} + +fn validateSource(program: *const program_ir.Program, source: operand.Source) Error!void { + try validateType(source.type); + if (source.region.width == 0) + return error.InvalidRegion; + try validateRegisterRef(program, source.register); + + if (source.register == .immediate) { + const matches = switch (source.register.immediate) { + .u32 => source.type == .u32, + .i32 => source.type == .i32, + .f32 => source.type == .f32, + }; + if (!matches) + return error.InvalidImmediateType; + } +} + +fn validateDestination(program: *const program_ir.Program, destination: operand.Destination) Error!void { + try validateType(destination.type); + if (destination.region.horizontal_stride == 0) + return error.InvalidRegion; + switch (destination.register) { + .immediate, .null => return error.InvalidDestination, + else => try validateRegisterRef(program, destination.register), + } +} + +fn validateRegisterRef(program: *const program_ir.Program, register: operand.RegisterRef) Error!void { + switch (register) { + .virtual => |id| if (!program.virtual_registers.isLive(id)) + return error.InvalidVirtualRegister, + .physical_grf => |physical| { + if (physical.number >= program.device_info.grf_count or + physical.byte_offset >= program.device_info.grf_size_bytes) + return error.InvalidPhysicalRegister; + }, + .architecture, .immediate, .null => {}, + } +} + +fn validateFlag(program: *const program_ir.Program, flag: operand.FlagRef) Error!void { + switch (flag) { + .virtual => |id| if (!program.virtual_flags.isLive(id)) + return error.InvalidVirtualFlag, + .physical => |physical| if (physical.register != 0 or physical.subregister > 1) + return error.InvalidPhysicalFlag, + } +} + +fn validateSpan(program: *const program_ir.Program, span: operand.RegisterSpan) Error!void { + if (span.register_count == 0) + return error.InvalidRegisterSpan; + switch (span.base) { + .virtual, .physical_grf => try validateRegisterRef(program, span.base), + else => return error.InvalidRegisterSpan, + } +} + +fn validateTerminator( + program: *const program_ir.Program, + function_id: ids.FunctionId, + terminator: instruction.Terminator, +) Error!void { + const function = program.functions.get(function_id) orelse return error.InvalidFunction; + switch (terminator) { + .jump => |target| try validateBlockTarget(program, function_id, target), + .conditional_branch => |branch| { + try validateFlag(program, branch.predicate.flag); + try validateBlockTarget(program, function_id, branch.true_block); + try validateBlockTarget(program, function_id, branch.false_block); + }, + .return_void => if (function.return_type != null) + return error.WrongReturnType, + .return_value => |value| { + const return_type = function.return_type orelse return error.WrongReturnType; + try validateSource(program, value); + if (value.type != return_type) + return error.WrongReturnType; + }, + .end_thread => if (program.entry_function != function_id) + return error.InvalidEntryTerminator, + .@"unreachable" => {}, + } +} + +fn validateStructuredControl( + program: *const program_ir.Program, + function_id: ids.FunctionId, + control: instruction.StructuredControl, +) Error!void { + switch (control) { + .none => {}, + .selection => |selection| try validateBlockTarget(program, function_id, selection.merge_block), + .loop => |loop| { + try validateBlockTarget(program, function_id, loop.merge_block); + try validateBlockTarget(program, function_id, loop.continue_block); + }, + } +} + +fn validateBlockTarget(program: *const program_ir.Program, function_id: ids.FunctionId, block_id: ids.BlockId) Error!void { + const block = program.blocks.get(block_id) orelse return error.InvalidBlock; + if (block.parent_function != function_id) + return error.CrossFunctionBranch; +} + +fn functionContainsBlock(program: *const program_ir.Program, function_id: ids.FunctionId, block_id: ids.BlockId) bool { + const function = program.functions.get(function_id) orelse return false; + for (function.blocks.items) |candidate| { + if (candidate == block_id) + return true; + } + return false; +} diff --git a/src/intel/lib.zig b/src/intel/lib.zig index 5146a75..a6a6010 100644 --- a/src/intel/lib.zig +++ b/src/intel/lib.zig @@ -32,6 +32,7 @@ pub const FlintQueryPool = @import("FlintQueryPool.zig"); pub const FlintRenderPass = @import("FlintRenderPass.zig"); pub const FlintSampler = @import("FlintSampler.zig"); pub const FlintShaderModule = @import("FlintShaderModule.zig"); +pub const compiler = @import("compiler/root.zig"); pub const Instance = FlintInstance; @@ -89,6 +90,7 @@ test { std.testing.refAllDecls(FlintRenderPass); std.testing.refAllDecls(FlintSampler); std.testing.refAllDecls(FlintShaderModule); + std.testing.refAllDecls(compiler); std.testing.refAllDecls(kmd); std.testing.refAllDecls(base); }