diff --git a/lib/std/builtin.zig b/lib/std/builtin.zig index 869756fe5c..1d28e69d61 100644 --- a/lib/std/builtin.zig +++ b/lib/std/builtin.zig @@ -160,8 +160,7 @@ pub const CallingConvention = enum { AAPCSVFP, SysV, Win64, - PtxKernel, - AmdgpuKernel, + Kernel, }; /// This data structure is used by the Zig language code generation and diff --git a/lib/std/start.zig b/lib/std/start.zig index 1b686e43f5..377a317df2 100644 --- a/lib/std/start.zig +++ b/lib/std/start.zig @@ -24,7 +24,9 @@ pub const simplified_logic = builtin.zig_backend == .stage2_aarch64 or builtin.zig_backend == .stage2_arm or builtin.zig_backend == .stage2_riscv64 or - builtin.zig_backend == .stage2_sparc64; + builtin.zig_backend == .stage2_sparc64 or + builtin.cpu.arch == .spirv32 or + builtin.cpu.arch == .spirv64; comptime { // No matter what, we import the root file, so that any export, test, comptime @@ -43,6 +45,9 @@ comptime { } } else if (builtin.os.tag == .wasi and @hasDecl(root, "main")) { @export(wasiMain2, .{ .name = "_start" }); + } else if (builtin.os.tag == .opencl) { + if (@hasDecl(root, "main")) + @export(spirvMain2, .{ .name = "main" }); } else { if (!@hasDecl(root, "_start")) { @export(_start2, .{ .name = "_start" }); @@ -127,6 +132,10 @@ fn wasiMain2() callconv(.C) noreturn { } } +fn spirvMain2() callconv(.Kernel) void { + root.main(); +} + fn wWinMainCRTStartup2() callconv(.C) noreturn { root.main(); exit2(0); diff --git a/lib/std/target.zig b/lib/std/target.zig index c02efa2495..fba399f02c 100644 --- a/lib/std/target.zig +++ b/lib/std/target.zig @@ -944,7 +944,7 @@ pub const Target = struct { }; } - pub fn isSPIRV(arch: Arch) bool { + pub fn isSpirV(arch: Arch) bool { return switch (arch) { .spirv32, .spirv64 => true, else => false, @@ -1276,7 +1276,7 @@ pub const Target = struct { .x86, .x86_64 => "x86", .nvptx, .nvptx64 => "nvptx", .wasm32, .wasm64 => "wasm", - .spirv32, .spirv64 => "spir-v", + .spirv32, .spirv64 => "spirv", else => @tagName(arch), }; } @@ -1329,6 +1329,7 @@ pub const Target = struct { .amdgcn => comptime allCpusFromDecls(amdgpu.cpu), .riscv32, .riscv64 => comptime allCpusFromDecls(riscv.cpu), .sparc, .sparc64, .sparcel => comptime allCpusFromDecls(sparc.cpu), + .spirv32, .spirv64 => comptime allCpusFromDecls(spirv.cpu), .s390x => comptime allCpusFromDecls(s390x.cpu), .x86, .x86_64 => comptime allCpusFromDecls(x86.cpu), .xtensa => comptime allCpusFromDecls(xtensa.cpu), @@ -1392,6 +1393,7 @@ pub const Target = struct { .amdgcn => &amdgpu.cpu.generic, .riscv32 => &riscv.cpu.generic_rv32, .riscv64 => &riscv.cpu.generic_rv64, + .spirv32, .spirv64 => &spirv.cpu.generic, .sparc, .sparcel => &sparc.cpu.generic, .sparc64 => &sparc.cpu.v9, // 64-bit SPARC needs v9 as the baseline .s390x => &s390x.cpu.generic, @@ -1532,6 +1534,10 @@ pub const Target = struct { return !self.cpu.arch.isWasm(); } + pub fn isSpirV(self: Target) bool { + return self.cpu.arch.isSpirV(); + } + pub const FloatAbi = enum { hard, soft, diff --git a/lib/std/target/spirv.zig b/lib/std/target/spirv.zig index 39a91c7537..b0f5a19461 100644 --- a/lib/std/target/spirv.zig +++ b/lib/std/target/spirv.zig @@ -2081,3 +2081,11 @@ pub const all_features = blk: { } break :blk result; }; + +pub const cpu = struct { + pub const generic = CpuModel{ + .name = "generic", + .llvm_name = "generic", + .features = featureSet(&[_]Feature{}), + }; +}; diff --git a/src/Compilation.zig b/src/Compilation.zig index f6f4851e5e..6eef76cf14 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -722,10 +722,11 @@ pub fn create(gpa: Allocator, options: InitOptions) !*Compilation { // Once they are capable this condition could be removed. When removing this condition, // also test the use case of `build-obj -fcompiler-rt` with the native backends // and make sure the compiler-rt symbols are emitted. - const capable_of_building_compiler_rt = build_options.have_llvm and options.target.os.tag != .plan9; - - const capable_of_building_zig_libc = build_options.have_llvm and options.target.os.tag != .plan9; - const capable_of_building_ssp = build_options.have_llvm and options.target.os.tag != .plan9; + const is_p9 = options.target.os.tag == .plan9; + const is_spv = options.target.cpu.arch.isSpirV(); + const capable_of_building_compiler_rt = build_options.have_llvm and !is_p9 and !is_spv; + const capable_of_building_zig_libc = build_options.have_llvm and !is_p9 and !is_spv; + const capable_of_building_ssp = build_options.have_llvm and !is_p9 and !is_spv; const comp: *Compilation = comp: { // For allocations that have the same lifetime as Compilation. This arena is used only during this @@ -1948,8 +1949,9 @@ pub fn update(comp: *Compilation, main_progress_node: *std.Progress.Node) !void .sub_path = std.fs.path.basename(sub_path), }; } - comp.bin_file.destroy(); + var old_bin_file = comp.bin_file; comp.bin_file = try link.File.openPath(comp.gpa, options); + old_bin_file.destroy(); } // For compiling C objects, we rely on the cache hash system to avoid duplicating work. diff --git a/src/Sema.zig b/src/Sema.zig index c3c9345205..e538334fcd 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -8883,7 +8883,7 @@ fn funcCommon( }; return sema.failWithOwnedErrorMsg(msg); } - if (!ret_poison and !Type.fnCallingConventionAllowsZigTypes(cc_resolved) and !try sema.validateExternType(return_type, .ret_ty)) { + if (!ret_poison and !Type.fnCallingConventionAllowsZigTypes(target, cc_resolved) and !try sema.validateExternType(return_type, .ret_ty)) { const msg = msg: { const msg = try sema.errMsg(block, ret_ty_src, "return type '{}' not allowed in function with calling convention '{s}'", .{ return_type.fmt(sema.mod), @tagName(cc_resolved), @@ -8961,13 +8961,9 @@ fn funcCommon( .x86_64 => null, else => @as([]const u8, "x86_64"), }, - .PtxKernel => switch (arch) { - .nvptx, .nvptx64 => null, - else => @as([]const u8, "nvptx and nvptx64"), - }, - .AmdgpuKernel => switch (arch) { - .amdgcn => null, - else => @as([]const u8, "amdgcn"), + .Kernel => switch (arch) { + .nvptx, .nvptx64, .amdgcn, .spirv32, .spirv64 => null, + else => @as([]const u8, "nvptx, amdgcn and SPIR-V"), }, }) |allowed_platform| { return sema.fail(block, cc_src, "callconv '{s}' is only available on {s}, not {s}", .{ @@ -9093,10 +9089,11 @@ fn analyzeParameter( comptime_params[i] = param.is_comptime or requires_comptime; const this_generic = param.ty.tag() == .generic_poison; is_generic.* = is_generic.* or this_generic; - if (param.is_comptime and !Type.fnCallingConventionAllowsZigTypes(cc)) { + const target = sema.mod.getTarget(); + if (param.is_comptime and !Type.fnCallingConventionAllowsZigTypes(target, cc)) { return sema.fail(block, param_src, "comptime parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)}); } - if (this_generic and !sema.no_partial_func_ty and !Type.fnCallingConventionAllowsZigTypes(cc)) { + if (this_generic and !sema.no_partial_func_ty and !Type.fnCallingConventionAllowsZigTypes(target, cc)) { return sema.fail(block, param_src, "generic parameters not allowed in function with calling convention '{s}'", .{@tagName(cc)}); } if (!param.ty.isValidParamType()) { @@ -9112,7 +9109,7 @@ fn analyzeParameter( }; return sema.failWithOwnedErrorMsg(msg); } - if (!this_generic and !Type.fnCallingConventionAllowsZigTypes(cc) and !try sema.validateExternType(param.ty, .param_ty)) { + if (!this_generic and !Type.fnCallingConventionAllowsZigTypes(target, cc) and !try sema.validateExternType(param.ty, .param_ty)) { const msg = msg: { const msg = try sema.errMsg(block, param_src, "parameter of type '{}' not allowed in function with calling convention '{s}'", .{ param.ty.fmt(sema.mod), @tagName(cc), @@ -22786,12 +22783,13 @@ fn validateExternType( }, .Fn => { if (position != .other) return false; - return switch (ty.fnCallingConvention()) { - // For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI. - // The goal is to experiment with more integrated CPU/GPU code. - .PtxKernel => true, - else => !Type.fnCallingConventionAllowsZigTypes(ty.fnCallingConvention()), - }; + const target = sema.mod.getTarget(); + // For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI. + // The goal is to experiment with more integrated CPU/GPU code. + if (ty.fnCallingConvention() == .Kernel and (target.cpu.arch == .nvptx or target.cpu.arch == .nvptx64)) { + return true; + } + return !Type.fnCallingConventionAllowsZigTypes(target, ty.fnCallingConvention()); }, .Enum => { var buf: Type.Payload.Bits = undefined; diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 233ec21ac1..80c9f0d024 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -10350,11 +10350,8 @@ fn toLlvmCallConv(cc: std.builtin.CallingConvention, target: std.Target) llvm.Ca .Signal => .AVR_SIGNAL, .SysV => .X86_64_SysV, .Win64 => .Win64, - .PtxKernel => return switch (target.cpu.arch) { + .Kernel => return switch (target.cpu.arch) { .nvptx, .nvptx64 => .PTX_Kernel, - else => unreachable, - }, - .AmdgpuKernel => return switch (target.cpu.arch) { .amdgcn => .AMDGPU_KERNEL, else => unreachable, }, diff --git a/src/codegen/spirv.zig b/src/codegen/spirv.zig index 5f27c14e95..58da5539ac 100644 --- a/src/codegen/spirv.zig +++ b/src/codegen/spirv.zig @@ -19,6 +19,7 @@ const Word = spec.Word; const IdRef = spec.IdRef; const IdResult = spec.IdResult; const IdResultType = spec.IdResultType; +const StorageClass = spec.StorageClass; const SpvModule = @import("spirv/Module.zig"); const SpvSection = @import("spirv/Section.zig"); @@ -32,11 +33,14 @@ const IncomingBlock = struct { break_value_id: IdRef, }; -pub const BlockMap = std.AutoHashMapUnmanaged(Air.Inst.Index, struct { +const BlockMap = std.AutoHashMapUnmanaged(Air.Inst.Index, struct { label_id: IdRef, incoming_blocks: *std.ArrayListUnmanaged(IncomingBlock), }); +/// Maps Zig decl indices to linking SPIR-V linking information. +pub const DeclLinkMap = std.AutoHashMap(Module.Decl.Index, SpvModule.Decl.Index); + /// This structure is used to compile a declaration, and contains all relevant meta-information to deal with that. pub const DeclGen = struct { /// A general-purpose allocator that can be used for any allocations for this DeclGen. @@ -59,7 +63,8 @@ pub const DeclGen = struct { /// Note: If the declaration is not a function, this value will be undefined! liveness: Liveness, - ids: *const std.AutoHashMap(Decl.Index, IdResult), + /// Maps Zig Decl indices to SPIR-V globals. + decl_link: *DeclLinkMap, /// An array of function argument result-ids. Each index corresponds with the /// function argument of the same index. @@ -133,13 +138,23 @@ pub const DeclGen = struct { class: Class, }; + /// Data can be lowered into in two basic representations: indirect, which is when + /// a type is stored in memory, and direct, which is how a type is stored when its + /// a direct SPIR-V value. + const Repr = enum { + /// A SPIR-V value as it would be used in operations. + direct, + /// A SPIR-V value as it is stored in memory. + indirect, + }; + /// Initialize the common resources of a DeclGen. Some fields are left uninitialized, /// only set when `gen` is called. pub fn init( allocator: Allocator, module: *Module, spv: *SpvModule, - ids: *const std.AutoHashMap(Decl.Index, IdResult), + decl_link: *DeclLinkMap, ) DeclGen { return .{ .gpa = allocator, @@ -148,7 +163,7 @@ pub const DeclGen = struct { .decl_index = undefined, .air = undefined, .liveness = undefined, - .ids = ids, + .decl_link = decl_link, .next_arg_index = undefined, .current_block_label_id = undefined, .error_msg = undefined, @@ -215,19 +230,50 @@ pub const DeclGen = struct { /// Fetch the result-id for a previously generated instruction or constant. fn resolve(self: *DeclGen, inst: Air.Inst.Ref) !IdRef { if (self.air.value(inst)) |val| { - return self.genConstant(self.air.typeOf(inst), val); + const ty = self.air.typeOf(inst); + if (ty.zigTypeTag() == .Fn) { + const fn_decl_index = switch (val.tag()) { + .extern_fn => val.castTag(.extern_fn).?.data.owner_decl, + .function => val.castTag(.function).?.data.owner_decl, + else => unreachable, + }; + const spv_decl_index = try self.resolveDecl(fn_decl_index); + return self.spv.declPtr(spv_decl_index).result_id; + } + + return try self.constant(ty, val); } const index = Air.refToIndex(inst).?; return self.inst_results.get(index).?; // Assertion means instruction does not dominate usage. } + /// Fetch or allocate a result id for decl index. This function also marks the decl as alive. + /// Note: Function does not actually generate the decl. + fn resolveDecl(self: *DeclGen, decl_index: Module.Decl.Index) !SpvModule.Decl.Index { + const decl = self.module.declPtr(decl_index); + self.module.markDeclAlive(decl); + + const entry = try self.decl_link.getOrPut(decl_index); + if (!entry.found_existing) { + // TODO: Extern fn? + const kind: SpvModule.DeclKind = if (decl.val.tag() == .function) + .func + else + .global; + + entry.value_ptr.* = try self.spv.allocDecl(kind); + } + + return entry.value_ptr.*; + } + /// Start a new SPIR-V block, Emits the label of the new block, and stores which /// block we are currently generating. /// Note that there is no such thing as nested blocks like in ZIR or AIR, so we don't need to /// keep track of the previous block. fn beginSpvBlock(self: *DeclGen, label_id: IdResult) !void { try self.func.body.emit(self.spv.gpa, .OpLabel, .{ .id_result = label_id }); - self.current_block_label_id = label_id.toRef(); + self.current_block_label_id = label_id; } /// SPIR-V requires enabling specific integer sizes through capabilities, and so if they are not enabled, we need @@ -325,150 +371,752 @@ pub const DeclGen = struct { // As of yet, there is no vector support in the self-hosted compiler. .Vector => self.todo("implement arithmeticTypeInfo for Vector", .{}), // TODO: For which types is this the case? - else => self.todo("implement arithmeticTypeInfo for {}", .{ty.fmtDebug()}), + else => self.todo("implement arithmeticTypeInfo for {}", .{ty.fmt(self.module)}), }; } - /// Generate a constant representing `val`. - /// TODO: Deduplication? - fn genConstant(self: *DeclGen, ty: Type, val: Value) Error!IdRef { - if (ty.zigTypeTag() == .Fn) { - const fn_decl_index = switch (val.tag()) { - .extern_fn => val.castTag(.extern_fn).?.data.owner_decl, - .function => val.castTag(.function).?.data.owner_decl, + fn genConstInt(self: *DeclGen, ty_ref: SpvType.Ref, result_id: IdRef, value: anytype) !void { + const ty = self.spv.typeRefType(ty_ref); + const ty_id = self.typeId(ty_ref); + + const Lit = spec.LiteralContextDependentNumber; + const literal = switch (ty.intSignedness()) { + .signed => switch (ty.intFloatBits()) { + 1...32 => Lit{ .int32 = @intCast(i32, value) }, + 33...64 => Lit{ .int64 = @intCast(i64, value) }, + else => unreachable, // TODO: composite integer literals + }, + .unsigned => switch (ty.intFloatBits()) { + 1...32 => Lit{ .uint32 = @intCast(u32, value) }, + 33...64 => Lit{ .uint64 = @intCast(u64, value) }, else => unreachable, - }; - const decl = self.module.declPtr(fn_decl_index); - self.module.markDeclAlive(decl); - return self.ids.get(fn_decl_index).?.toRef(); + }, + }; + + try self.spv.emitConstant(ty_id, result_id, literal); + } + + fn constInt(self: *DeclGen, ty_ref: SpvType.Ref, value: anytype) !IdRef { + const result_id = self.spv.allocId(); + try self.genConstInt(ty_ref, result_id, value); + return result_id; + } + + fn genUndef(self: *DeclGen, ty_ref: SpvType.Ref) Error!IdRef { + const result_id = self.spv.allocId(); + try self.spv.sections.types_globals_constants.emit(self.spv.gpa, .OpUndef, .{ .id_result_type = self.typeId(ty_ref), .id_result = result_id }); + return result_id; + } + + const IndirectConstantLowering = struct { + const undef = 0xAA; + + dg: *DeclGen, + /// Cached reference of the u32 type. + u32_ty_ref: SpvType.Ref, + /// Cached type id of the u32 type. + u32_ty_id: IdRef, + /// The members of the resulting structure type + members: std.ArrayList(SpvType.Payload.Struct.Member), + /// The initializers of each of the members. + initializers: std.ArrayList(IdRef), + /// The current size of the structure. Includes + /// the bytes in partial_word. + size: u32 = 0, + /// The partially filled last constant. + /// If full, its flushed. + partial_word: std.BoundedArray(u8, @sizeOf(Word)) = .{}, + /// The declaration dependencies of the constant we are lowering. + decl_deps: std.ArrayList(SpvModule.Decl.Index), + + /// Utility function to get the section that instructions should be lowered to. + fn section(self: *@This()) *SpvSection { + return &self.dg.spv.globals.section; } + /// Flush the partial_word to the members. If the partial_word is not + /// filled, this adds padding bytes (which are undefined). + fn flush(self: *@This()) !void { + if (self.partial_word.len == 0) { + // No need to add it there. + return; + } + + for (self.partial_word.unusedCapacitySlice()) |*unused| { + // TODO: Perhaps we should generate OpUndef for these bytes? + unused.* = undef; + } + + const word = @bitCast(Word, self.partial_word.buffer); + const result_id = self.dg.spv.allocId(); + // TODO: Integrate with caching mechanism + try self.dg.spv.emitConstant(self.u32_ty_id, result_id, .{ .uint32 = word }); + try self.members.append(.{ .ty = self.u32_ty_ref }); + try self.initializers.append(result_id); + + self.partial_word.len = 0; + self.size = std.mem.alignForwardGeneric(u32, self.size, @sizeOf(Word)); + } + + /// Fill the buffer with undefined values until the size is aligned to `align`. + fn fillToAlign(self: *@This(), alignment: u32) !void { + const target_size = std.mem.alignForwardGeneric(u32, self.size, alignment); + try self.addUndef(target_size - self.size); + } + + fn addUndef(self: *@This(), amt: u64) !void { + for (0..@intCast(usize, amt)) |_| { + try self.addByte(undef); + } + } + + /// Add a single byte of data to the constant. + fn addByte(self: *@This(), data: u8) !void { + self.partial_word.append(data) catch { + try self.flush(); + self.partial_word.append(data) catch unreachable; + }; + self.size += 1; + } + + /// Add many bytes of data to the constnat. + fn addBytes(self: *@This(), data: []const u8) !void { + // TODO: Improve performance by adding in bulk, or something? + for (data) |byte| { + try self.addByte(byte); + } + } + + fn addPtr(self: *@This(), ptr_ty_ref: SpvType.Ref, ptr_id: IdRef) !void { + // TODO: Double check pointer sizes here. + // shared pointers might be u32... + const target = self.dg.getTarget(); + const width = @divExact(target.cpu.arch.ptrBitWidth(), 8); + if (self.size % width != 0) { + return self.dg.todo("misaligned pointer constants", .{}); + } + try self.members.append(.{ .ty = ptr_ty_ref }); + try self.initializers.append(ptr_id); + self.size += width; + } + + fn addNullPtr(self: *@This(), ptr_ty_ref: SpvType.Ref) !void { + const result_id = self.dg.spv.allocId(); + try self.dg.spv.sections.types_globals_constants.emit(self.dg.spv.gpa, .OpConstantNull, .{ + .id_result_type = self.dg.typeId(ptr_ty_ref), + .id_result = result_id, + }); + try self.addPtr(ptr_ty_ref, result_id); + } + + fn addConstInt(self: *@This(), comptime T: type, value: T) !void { + if (@bitSizeOf(T) % 8 != 0) { + @compileError("todo: non byte aligned int constants"); + } + + // TODO: Swap endianness if the compiler is big endian. + try self.addBytes(std.mem.asBytes(&value)); + } + + fn addConstBool(self: *@This(), value: bool) !void { + try self.addByte(@boolToInt(value)); // TODO: Keep in sync with something? + } + + fn addInt(self: *@This(), ty: Type, val: Value) !void { + const target = self.dg.getTarget(); + const int_info = ty.intInfo(target); + const int_bits = switch (int_info.signedness) { + .signed => @bitCast(u64, val.toSignedInt(target)), + .unsigned => val.toUnsignedInt(target), + }; + + // TODO: Swap endianess if the compiler is big endian. + const len = ty.abiSize(target); + try self.addBytes(std.mem.asBytes(&int_bits)[0..@intCast(usize, len)]); + } + + fn addDeclRef(self: *@This(), ty: Type, decl_index: Decl.Index) !void { + const dg = self.dg; + + const ty_ref = try self.dg.resolveType(ty, .indirect); + const ty_id = dg.typeId(ty_ref); + + const decl = dg.module.declPtr(decl_index); + const spv_decl_index = try dg.resolveDecl(decl_index); + + switch (decl.val.tag()) { + .function => { + // TODO: Properly lower function pointers. For now we are going to hack around it and + // just generate an empty pointer. Function pointers are represented by usize for now, + // though. + try self.addInt(Type.usize, Value.initTag(.zero)); + return; + }, + .extern_fn => unreachable, // TODO + else => { + const result_id = dg.spv.allocId(); + log.debug("addDeclRef {s} = {}", .{ decl.name, result_id.id }); + + try self.decl_deps.append(spv_decl_index); + + const decl_id = dg.spv.declPtr(spv_decl_index).result_id; + // TODO: Do we need a storage class cast here? + // TODO: We can probably eliminate these casts + try dg.spv.globals.section.emitSpecConstantOp(dg.spv.gpa, .OpBitcast, .{ + .id_result_type = ty_id, + .id_result = result_id, + .operand = decl_id, + }); + + try self.addPtr(ty_ref, result_id); + }, + } + } + + fn lower(self: *@This(), ty: Type, val: Value) !void { + const target = self.dg.getTarget(); + const dg = self.dg; + + if (val.isUndef()) { + const size = ty.abiSize(target); + return try self.addUndef(size); + } + + switch (ty.zigTypeTag()) { + .Int => try self.addInt(ty, val), + .Bool => try self.addConstBool(val.toBool()), + .Array => switch (val.tag()) { + .aggregate => { + const elem_vals = val.castTag(.aggregate).?.data; + const elem_ty = ty.elemType(); + const len = @intCast(u32, ty.arrayLenIncludingSentinel()); // TODO: limit spir-v to 32 bit arrays in a more elegant way. + for (elem_vals[0..len]) |elem_val| { + try self.lower(elem_ty, elem_val); + } + }, + .repeated => { + const elem_val = val.castTag(.repeated).?.data; + const elem_ty = ty.elemType(); + const len = @intCast(u32, ty.arrayLen()); + for (0..len) |_| { + try self.lower(elem_ty, elem_val); + } + if (ty.sentinel()) |sentinel| { + try self.lower(elem_ty, sentinel); + } + }, + .str_lit => { + const str_lit = val.castTag(.str_lit).?.data; + const bytes = dg.module.string_literal_bytes.items[str_lit.index..][0..str_lit.len]; + try self.addBytes(bytes); + if (ty.sentinel()) |sentinel| { + try self.addByte(@intCast(u8, sentinel.toUnsignedInt(target))); + } + }, + .bytes => { + const bytes = val.castTag(.bytes).?.data; + try self.addBytes(bytes); + }, + else => |tag| return dg.todo("indirect array constant with tag {s}", .{@tagName(tag)}), + }, + .Pointer => switch (val.tag()) { + .decl_ref_mut => { + const decl_index = val.castTag(.decl_ref_mut).?.data.decl_index; + try self.addDeclRef(ty, decl_index); + }, + .decl_ref => { + const decl_index = val.castTag(.decl_ref).?.data; + try self.addDeclRef(ty, decl_index); + }, + .slice => { + const slice = val.castTag(.slice).?.data; + + var buf: Type.SlicePtrFieldTypeBuffer = undefined; + const ptr_ty = ty.slicePtrFieldType(&buf); + + try self.lower(ptr_ty, slice.ptr); + try self.addInt(Type.usize, slice.len); + }, + else => |tag| return dg.todo("pointer value of type {s}", .{@tagName(tag)}), + }, + .Struct => { + if (ty.isSimpleTupleOrAnonStruct()) { + unreachable; // TODO + } else { + const struct_ty = ty.castTag(.@"struct").?.data; + + if (struct_ty.layout == .Packed) { + return dg.todo("packed struct constants", .{}); + } + + const struct_begin = self.size; + const field_vals = val.castTag(.aggregate).?.data; + for (struct_ty.fields.values(), 0..) |field, i| { + if (field.is_comptime or !field.ty.hasRuntimeBits()) continue; + try self.lower(field.ty, field_vals[i]); + + // Add padding if required. + // TODO: Add to type generation as well? + const unpadded_field_end = self.size - struct_begin; + const padded_field_end = ty.structFieldOffset(i + 1, target); + const padding = padded_field_end - unpadded_field_end; + try self.addUndef(padding); + } + } + }, + .Optional => { + var opt_buf: Type.Payload.ElemType = undefined; + const payload_ty = ty.optionalChild(&opt_buf); + const has_payload = !val.isNull(); + const abi_size = ty.abiSize(target); + + if (!payload_ty.hasRuntimeBits()) { + try self.addConstBool(has_payload); + return; + } else if (ty.optionalReprIsPayload()) { + // Optional representation is a nullable pointer. + if (val.castTag(.opt_payload)) |payload| { + try self.lower(payload_ty, payload.data); + } else if (has_payload) { + try self.lower(payload_ty, val); + } else { + const ptr_ty_ref = try dg.resolveType(ty, .indirect); + try self.addNullPtr(ptr_ty_ref); + } + return; + } + + // Optional representation is a structure. + // { Payload, Bool } + + // Subtract 1 for @sizeOf(bool). + // TODO: Make this not hardcoded. + const payload_size = payload_ty.abiSize(target); + const padding = abi_size - payload_size - 1; + + if (val.castTag(.opt_payload)) |payload| { + try self.lower(payload_ty, payload.data); + } else { + try self.addUndef(payload_size); + } + try self.addConstBool(has_payload); + try self.addUndef(padding); + }, + .Enum => { + var int_val_buffer: Value.Payload.U64 = undefined; + const int_val = val.enumToInt(ty, &int_val_buffer); + + var int_ty_buffer: Type.Payload.Bits = undefined; + const int_ty = ty.intTagType(&int_ty_buffer); + + try self.lower(int_ty, int_val); + }, + .Union => { + const tag_and_val = val.castTag(.@"union").?.data; + const layout = ty.unionGetLayout(target); + + if (layout.payload_size == 0) { + return try self.lower(ty.unionTagTypeSafety().?, tag_and_val.tag); + } + + const union_ty = ty.cast(Type.Payload.Union).?.data; + if (union_ty.layout == .Packed) { + return dg.todo("packed union constants", .{}); + } + + const active_field = ty.unionTagFieldIndex(tag_and_val.tag, dg.module).?; + const active_field_ty = union_ty.fields.values()[active_field].ty; + + const has_tag = layout.tag_size != 0; + const tag_first = layout.tag_align >= layout.payload_align; + + if (has_tag and tag_first) { + try self.lower(ty.unionTagTypeSafety().?, tag_and_val.tag); + } + + const active_field_size = if (active_field_ty.hasRuntimeBitsIgnoreComptime()) blk: { + try self.lower(active_field_ty, tag_and_val.val); + break :blk active_field_ty.abiSize(target); + } else 0; + + const payload_padding_len = layout.payload_size - active_field_size; + try self.addUndef(payload_padding_len); + + if (has_tag and !tag_first) { + try self.lower(ty.unionTagTypeSafety().?, tag_and_val.tag); + } + + try self.addUndef(layout.padding); + }, + .ErrorSet => switch (val.tag()) { + .@"error" => { + const err_name = val.castTag(.@"error").?.data.name; + const kv = try dg.module.getErrorValue(err_name); + try self.addConstInt(u16, @intCast(u16, kv.value)); + }, + .zero => { + // Unactivated error set. + try self.addConstInt(u16, 0); + }, + else => unreachable, + }, + .ErrorUnion => { + const payload_ty = ty.errorUnionPayload(); + const is_pl = val.errorUnionIsPayload(); + const error_val = if (!is_pl) val else Value.initTag(.zero); + + if (!payload_ty.hasRuntimeBitsIgnoreComptime()) { + return try self.lower(Type.anyerror, error_val); + } + + const payload_align = payload_ty.abiAlignment(target); + const error_align = Type.anyerror.abiAlignment(target); + + const payload_size = payload_ty.abiSize(target); + const error_size = Type.anyerror.abiAlignment(target); + const ty_size = ty.abiSize(target); + const padding = ty_size - payload_size - error_size; + + const payload_val = if (val.castTag(.eu_payload)) |pl| pl.data else Value.initTag(.undef); + + if (error_align > payload_align) { + try self.lower(Type.anyerror, error_val); + try self.lower(payload_ty, payload_val); + } else { + try self.lower(payload_ty, payload_val); + try self.lower(Type.anyerror, error_val); + } + + try self.addUndef(padding); + }, + else => |tag| return dg.todo("indirect constant of type {s}", .{@tagName(tag)}), + } + } + }; + + /// Returns a pointer to `val`. The value is placed directly + /// into the storage class `storage_class`, and this is also where the resulting + /// pointer points to. Note: result is not necessarily an OpVariable instruction! + fn lowerIndirectConstant( + self: *DeclGen, + spv_decl_index: SpvModule.Decl.Index, + ty: Type, + val: Value, + storage_class: StorageClass, + cast_to_generic: bool, + alignment: u32, + ) Error!void { + // To simplify constant generation, we're going to generate constants as a word-array, and + // pointer cast the result to the right type. + // This means that the final constant will be generated as follows: + // %T = OpTypeStruct %members... + // %P = OpTypePointer %T + // %U = OpTypePointer %ty + // %1 = OpConstantComposite %T %initializers... + // %2 = OpVariable %P %1 + // %result_id = OpSpecConstantOp OpBitcast %U %2 + // + // The members consist of two options: + // - Literal values: ints, strings, etc. These are generated as u32 words. + // - Relocations, such as pointers: These are generated by embedding the pointer into the + // to-be-generated structure. There are two options here, depending on the alignment of the + // pointer value itself (not the alignment of the pointee). + // - Natively or over-aligned values. These can just be generated directly. + // - Underaligned pointers. These need to be packed into the word array by using a mixture of + // OpSpecConstantOp instructions such as OpConvertPtrToU, OpBitcast, OpShift, etc. + + // TODO: Implement alignment here. + // This is hoing to require some hacks because there is no real way to + // set an OpVariable's alignment. + _ = alignment; + + assert(storage_class != .Generic and storage_class != .Function); + + log.debug("lowerIndirectConstant: ty = {}, val = {}", .{ ty.fmt(self.module), val.fmtDebug() }); + + const section = &self.spv.globals.section; + + const ty_ref = try self.resolveType(ty, .indirect); + const ptr_ty_ref = try self.spv.ptrType(ty_ref, storage_class, 0); + + // const target = self.getTarget(); + + // TODO: Fix the resulting global linking for these paths. + // if (val.isUndef()) { + // // Special case: the entire value is undefined. In this case, we can just + // // generate an OpVariable with no initializer. + // return try section.emit(self.spv.gpa, .OpVariable, .{ + // .id_result_type = self.typeId(ptr_ty_ref), + // .id_result = result_id, + // .storage_class = storage_class, + // }); + // } else if (ty.abiSize(target) == 0) { + // // Special case: if the type has no size, then return an undefined pointer. + // return try section.emit(self.spv.gpa, .OpUndef, .{ + // .id_result_type = self.typeId(ptr_ty_ref), + // .id_result = result_id, + // }); + // } + + // TODO: Capture the above stuff in here as well... + const begin_inst = self.spv.beginGlobal(); + + const u32_ty_ref = try self.intType(.unsigned, 32); + var icl = IndirectConstantLowering{ + .dg = self, + .u32_ty_ref = u32_ty_ref, + .u32_ty_id = self.typeId(u32_ty_ref), + .members = std.ArrayList(SpvType.Payload.Struct.Member).init(self.gpa), + .initializers = std.ArrayList(IdRef).init(self.gpa), + .decl_deps = std.ArrayList(SpvModule.Decl.Index).init(self.gpa), + }; + + defer icl.members.deinit(); + defer icl.initializers.deinit(); + defer icl.decl_deps.deinit(); + + try icl.lower(ty, val); + try icl.flush(); + + const constant_struct_ty_ref = try self.spv.simpleStructType(icl.members.items); + const ptr_constant_struct_ty_ref = try self.spv.ptrType(constant_struct_ty_ref, storage_class, 0); + + const constant_struct_id = self.spv.allocId(); + try section.emit(self.spv.gpa, .OpSpecConstantComposite, .{ + .id_result_type = self.typeId(constant_struct_ty_ref), + .id_result = constant_struct_id, + .constituents = icl.initializers.items, + }); + + const var_id = self.spv.allocId(); + self.spv.globalPtr(spv_decl_index).?.result_id = var_id; + try section.emit(self.spv.gpa, .OpVariable, .{ + .id_result_type = self.typeId(ptr_constant_struct_ty_ref), + .id_result = var_id, + .storage_class = storage_class, + .initializer = constant_struct_id, + }); + // TODO: Set alignment of OpVariable. + // TODO: We may be able to eliminate these casts. + + const const_ptr_id = try self.makePointerConstant(section, ptr_constant_struct_ty_ref, var_id); + const result_id = self.spv.declPtr(spv_decl_index).result_id; + + const bitcast_result_id = if (cast_to_generic) + self.spv.allocId() + else + result_id; + + try section.emitSpecConstantOp(self.spv.gpa, .OpBitcast, .{ + .id_result_type = self.typeId(ptr_ty_ref), + .id_result = bitcast_result_id, + .operand = const_ptr_id, + }); + + if (cast_to_generic) { + const generic_ptr_ty_ref = try self.spv.ptrType(ty_ref, .Generic, 0); + try section.emitSpecConstantOp(self.spv.gpa, .OpPtrCastToGeneric, .{ + .id_result_type = self.typeId(generic_ptr_ty_ref), + .id_result = result_id, + .pointer = bitcast_result_id, + }); + } + + try self.spv.declareDeclDeps(spv_decl_index, icl.decl_deps.items); + self.spv.endGlobal(spv_decl_index, begin_inst); + } + + /// This function generates a load for a constant in direct (ie, non-memory) representation. + /// When the constant is simple, it can be generated directly using OpConstant instructions. When + /// the constant is more complicated however, it needs to be lowered to an indirect constant, which + /// is then loaded using OpLoad. Such values are loaded into the UniformConstant storage class by default. + /// This function should only be called during function code generation. + fn constant(self: *DeclGen, ty: Type, val: Value) !IdRef { const target = self.getTarget(); const section = &self.spv.sections.types_globals_constants; + const result_ty_ref = try self.resolveType(ty, .direct); + const result_ty_id = self.typeId(result_ty_ref); const result_id = self.spv.allocId(); - const result_type_id = try self.resolveTypeId(ty); if (val.isUndef()) { - try section.emit(self.spv.gpa, .OpUndef, .{ .id_result_type = result_type_id, .id_result = result_id }); - return result_id.toRef(); + try section.emit(self.spv.gpa, .OpUndef, .{ + .id_result_type = result_ty_id, + .id_result = result_id, + }); + return result_id; } switch (ty.zigTypeTag()) { .Int => { - const int_info = ty.intInfo(target); - const backing_bits = self.backingIntBits(int_info.bits) orelse { - // Integers too big for any native type are represented as "composite integers": An array of largestSupportedIntBits. - return self.todo("implement composite int constants for {}", .{ty.fmtDebug()}); - }; - - // We can just use toSignedInt/toUnsignedInt here as it returns u64 - a type large enough to hold any - // SPIR-V native type (up to i/u64 with Int64). If SPIR-V ever supports native ints of a larger size, this - // might need to be updated. - assert(self.largestSupportedIntBits() <= @bitSizeOf(u64)); - - // Note, value is required to be sign-extended, so we don't need to mask off the upper bits. - // See https://www.khronos.org/registry/SPIR-V/specs/unified1/SPIRV.html#Literal - var int_bits = if (ty.isSignedInt()) @bitCast(u64, val.toSignedInt(target)) else val.toUnsignedInt(target); - - const value: spec.LiteralContextDependentNumber = switch (backing_bits) { - 1...32 => .{ .uint32 = @truncate(u32, int_bits) }, - 33...64 => .{ .uint64 = int_bits }, - else => unreachable, - }; - - try section.emit(self.spv.gpa, .OpConstant, .{ - .id_result_type = result_type_id, - .id_result = result_id, - .value = value, - }); + const int_bits = if (ty.isSignedInt()) + @bitCast(u64, val.toSignedInt(target)) + else + val.toUnsignedInt(target); + try self.genConstInt(result_ty_ref, result_id, int_bits); }, .Bool => { - const operands = .{ .id_result_type = result_type_id, .id_result = result_id }; + const operands = .{ .id_result_type = result_ty_id, .id_result = result_id }; if (val.toBool()) { try section.emit(self.spv.gpa, .OpConstantTrue, operands); } else { try section.emit(self.spv.gpa, .OpConstantFalse, operands); } }, - .Float => { - // At this point we are guaranteed that the target floating point type is supported, otherwise the function - // would have exited at resolveTypeId(ty). + // TODO: We can handle most pointers here (decl refs etc), because now they emit an extra + // OpVariable that is not really required. + else => { + // The value cannot be generated directly, so generate it as an indirect constant, + // and then perform an OpLoad. + const alignment = ty.abiAlignment(target); + const spv_decl_index = try self.spv.allocDecl(.global); - const value: spec.LiteralContextDependentNumber = switch (ty.floatBits(target)) { - // Prevent upcasting to f32 by bitcasting and writing as a uint32. - 16 => .{ .uint32 = @bitCast(u16, val.toFloat(f16)) }, - 32 => .{ .float32 = val.toFloat(f32) }, - 64 => .{ .float64 = val.toFloat(f64) }, - 128 => unreachable, // Filtered out in the call to resolveTypeId. - // TODO: Insert case for long double when the layout for that is determined? - else => unreachable, - }; + try self.lowerIndirectConstant( + spv_decl_index, + ty, + val, + .UniformConstant, + false, + alignment, + ); + try self.func.decl_deps.append(self.spv.gpa, spv_decl_index); - try section.emit(self.spv.gpa, .OpConstant, .{ - .id_result_type = result_type_id, + try self.func.body.emit(self.spv.gpa, .OpLoad, .{ + .id_result_type = result_ty_id, .id_result = result_id, - .value = value, + .pointer = self.spv.declPtr(spv_decl_index).result_id, }); + // TODO: Convert bools? This logic should hook into `load`. It should be a dead + // path though considering .Bool is handled above. }, - .Vector => switch (val.tag()) { - .aggregate => { - const elem_vals = val.castTag(.aggregate).?.data; - const vector_len = @intCast(usize, ty.vectorLen()); - const elem_ty = ty.elemType(); - - const elem_refs = try self.gpa.alloc(IdRef, vector_len); - defer self.gpa.free(elem_refs); - for (elem_refs, 0..) |*elem, i| { - elem.* = try self.genConstant(elem_ty, elem_vals[i]); - } - try section.emit(self.spv.gpa, .OpConstantComposite, .{ - .id_result_type = result_type_id, - .id_result = result_id, - .constituents = elem_refs, - }); - }, - else => unreachable, // TODO - }, - .Void => unreachable, - .Fn => unreachable, - else => return self.todo("constant generation of type {}", .{ty.fmtDebug()}), } - return result_id.toRef(); + return result_id; } /// Turn a Zig type into a SPIR-V Type, and return its type result-id. fn resolveTypeId(self: *DeclGen, ty: Type) !IdResultType { - const type_ref = try self.resolveType(ty); - return self.spv.typeResultId(type_ref); + const type_ref = try self.resolveType(ty, .direct); + return self.typeId(type_ref); + } + + fn typeId(self: *DeclGen, ty_ref: SpvType.Ref) IdRef { + return self.spv.typeId(ty_ref); + } + + /// Create an integer type suitable for storing at least 'bits' bits. + fn intType(self: *DeclGen, signedness: std.builtin.Signedness, bits: u16) !SpvType.Ref { + const backing_bits = self.backingIntBits(bits) orelse { + // TODO: Integers too big for any native type are represented as "composite integers": + // An array of largestSupportedIntBits. + return self.todo("Implement {s} composite int type of {} bits", .{ @tagName(signedness), bits }); + }; + + return try self.spv.resolveType(try SpvType.int(self.spv.arena, signedness, backing_bits)); + } + + /// Create an integer type that represents 'usize'. + fn sizeType(self: *DeclGen) !SpvType.Ref { + return try self.intType(.unsigned, self.getTarget().cpu.arch.ptrBitWidth()); + } + + /// Generate a union type, optionally with a known field. If the tag alignment is greater + /// than that of the payload, a regular union (non-packed, with both tag and payload), will + /// be generated as follows: + /// If the active field is known: + /// struct { + /// tag: TagType, + /// payload: ActivePayloadType, + /// payload_padding: [payload_size - @sizeOf(ActivePayloadType)]u8, + /// padding: [padding_size]u8, + /// } + /// If the payload alignment is greater than that of the tag: + /// struct { + /// payload: ActivePayloadType, + /// payload_padding: [payload_size - @sizeOf(ActivePayloadType)]u8, + /// tag: TagType, + /// padding: [padding_size]u8, + /// } + /// If the active payload is unknown, it will default back to the most aligned field. This is + /// to make sure that the overal struct has the correct alignment in spir-v. + /// If any of the fields' size is 0, it will be omitted. + /// NOTE: When the active field is set to something other than the most aligned field, the + /// resulting struct will be *underaligned*. + fn resolveUnionType(self: *DeclGen, ty: Type, maybe_active_field: ?usize) !SpvType.Ref { + const target = self.getTarget(); + const layout = ty.unionGetLayout(target); + const union_ty = ty.cast(Type.Payload.Union).?.data; + + if (union_ty.layout == .Packed) { + return self.todo("packed union types", .{}); + } + + const tag_ty_ref = try self.resolveType(union_ty.tag_ty, .indirect); + if (layout.payload_size == 0) { + // No payload, so represent this as just the tag type. + return tag_ty_ref; + } + + var members = std.BoundedArray(SpvType.Payload.Struct.Member, 4){}; + + const has_tag = layout.tag_size != 0; + const tag_first = layout.tag_align >= layout.payload_align; + const tag_member = .{ .name = "tag", .ty = tag_ty_ref }; + const u8_ty_ref = try self.intType(.unsigned, 8); // TODO: What if Int8Type is not enabled? + + if (has_tag and tag_first) { + members.appendAssumeCapacity(tag_member); + } + + const active_field = maybe_active_field orelse layout.most_aligned_field; + const active_field_ty = union_ty.fields.values()[active_field].ty; + + const active_field_size = if (active_field_ty.hasRuntimeBitsIgnoreComptime()) blk: { + const active_payload_ty_ref = try self.resolveType(active_field_ty, .indirect); + members.appendAssumeCapacity(.{ .name = "payload", .ty = active_payload_ty_ref }); + break :blk active_field_ty.abiSize(target); + } else 0; + + const payload_padding_len = layout.payload_size - active_field_size; + if (payload_padding_len != 0) { + const payload_padding_ty_ref = try self.spv.arrayType(@intCast(u32, payload_padding_len), u8_ty_ref); + members.appendAssumeCapacity(.{ .name = "padding_payload", .ty = payload_padding_ty_ref }); + } + + if (has_tag and !tag_first) { + members.appendAssumeCapacity(tag_member); + } + + if (layout.padding != 0) { + const padding_ty_ref = try self.spv.arrayType(layout.padding, u8_ty_ref); + members.appendAssumeCapacity(.{ .name = "padding", .ty = padding_ty_ref }); + } + + return try self.spv.simpleStructType(members.slice()); } /// Turn a Zig type into a SPIR-V Type, and return a reference to it. - fn resolveType(self: *DeclGen, ty: Type) Error!SpvType.Ref { + fn resolveType(self: *DeclGen, ty: Type, repr: Repr) Error!SpvType.Ref { + log.debug("resolveType: ty = {}", .{ty.fmt(self.module)}); const target = self.getTarget(); - return switch (ty.zigTypeTag()) { - .Void => try self.spv.resolveType(SpvType.initTag(.void)), - .Bool => blk: { - // TODO: SPIR-V booleans are opaque. For local variables this is fine, but for structs - // members we want to use integer types instead. - break :blk try self.spv.resolveType(SpvType.initTag(.bool)); + switch (ty.zigTypeTag()) { + .Void, .NoReturn => return try self.spv.resolveType(SpvType.initTag(.void)), + .Bool => switch (repr) { + .direct => return try self.spv.resolveType(SpvType.initTag(.bool)), + // SPIR-V booleans are opaque, which is fine for operations, but they cant be stored. + // This function returns the *stored* type, for values directly we convert this into a bool when + // it is loaded, and convert it back to this type when stored. + .indirect => return try self.intType(.unsigned, 1), }, - .Int => blk: { + .Int => { const int_info = ty.intInfo(target); - const backing_bits = self.backingIntBits(int_info.bits) orelse { - // TODO: Integers too big for any native type are represented as "composite integers": - // An array of largestSupportedIntBits. - return self.todo("Implement composite int type {}", .{ty.fmtDebug()}); - }; - - const payload = try self.spv.arena.create(SpvType.Payload.Int); - payload.* = .{ - .width = backing_bits, - .signedness = int_info.signedness, - }; - break :blk try self.spv.resolveType(SpvType.initPayload(&payload.base)); + return try self.intType(int_info.signedness, int_info.bits); }, - .Float => blk: { + .Enum => { + var buffer: Type.Payload.Bits = undefined; + const tag_ty = ty.intTagType(&buffer); + return self.resolveType(tag_ty, repr); + }, + .Float => { // We can (and want) not really emulate floating points with other floating point types like with the integer types, // so if the float is not supported, just return an error. const bits = ty.floatBits(target); @@ -484,43 +1132,58 @@ pub const DeclGen = struct { return self.fail("Floating point width of {} bits is not supported for the current SPIR-V feature set", .{bits}); } - const payload = try self.spv.arena.create(SpvType.Payload.Float); - payload.* = .{ - .width = bits, - }; - break :blk try self.spv.resolveType(SpvType.initPayload(&payload.base)); + return try self.spv.resolveType(SpvType.float(bits)); }, - .Fn => blk: { - // We only support C-calling-convention functions for now, no varargs. - if (ty.fnCallingConvention() != .C) - return self.fail("Unsupported calling convention for SPIR-V", .{}); - if (ty.fnIsVarArgs()) - return self.fail("VarArgs functions are unsupported for SPIR-V", .{}); + .Array => { + const elem_ty = ty.childType(); + const elem_ty_ref = try self.resolveType(elem_ty, .indirect); + const total_len = std.math.cast(u32, ty.arrayLenIncludingSentinel()) orelse { + return self.fail("array type of {} elements is too large", .{ty.arrayLenIncludingSentinel()}); + }; + return try self.spv.arrayType(total_len, elem_ty_ref); + }, + .Fn => switch (repr) { + .direct => { + // TODO: Put this somewhere in Sema.zig + if (ty.fnIsVarArgs()) + return self.fail("VarArgs functions are unsupported for SPIR-V", .{}); - const param_types = try self.spv.arena.alloc(SpvType.Ref, ty.fnParamLen()); - for (param_types, 0..) |*param, i| { - param.* = try self.resolveType(ty.fnParamType(i)); + // TODO: Parameter passing convention etc. + + const param_types = try self.spv.arena.alloc(SpvType.Ref, ty.fnParamLen()); + for (param_types, 0..) |*param, i| { + param.* = try self.resolveType(ty.fnParamType(i), .direct); + } + + const return_type = try self.resolveType(ty.fnReturnType(), .direct); + + const payload = try self.spv.arena.create(SpvType.Payload.Function); + payload.* = .{ .return_type = return_type, .parameters = param_types }; + return try self.spv.resolveType(SpvType.initPayload(&payload.base)); + }, + .indirect => { + // TODO: Represent function pointers properly. + // For now, just use an usize type. + return try self.sizeType(); + }, + }, + .Pointer => { + const ptr_info = ty.ptrInfo().data; + + const storage_class = spvStorageClass(ptr_info.@"addrspace"); + const child_ty_ref = try self.resolveType(ptr_info.pointee_type, .indirect); + const ptr_ty_ref = try self.spv.ptrType(child_ty_ref, storage_class, 0); + + if (ptr_info.size != .Slice) { + return ptr_ty_ref; } - const return_type = try self.resolveType(ty.fnReturnType()); - - const payload = try self.spv.arena.create(SpvType.Payload.Function); - payload.* = .{ .return_type = return_type, .parameters = param_types }; - break :blk try self.spv.resolveType(SpvType.initPayload(&payload.base)); + return try self.spv.simpleStructType(&.{ + .{ .ty = ptr_ty_ref, .name = "ptr" }, + .{ .ty = try self.sizeType(), .name = "len" }, + }); }, - .Pointer => blk: { - const payload = try self.spv.arena.create(SpvType.Payload.Pointer); - payload.* = .{ - .storage_class = spirvStorageClass(ty.ptrAddressSpace()), - .child_type = try self.resolveType(ty.elemType()), - .array_stride = 0, - // Note: only available in Kernels! - .alignment = null, - .max_byte_offset = null, - }; - break :blk try self.spv.resolveType(SpvType.initPayload(&payload.base)); - }, - .Vector => blk: { + .Vector => { // Although not 100% the same, Zig vectors map quite neatly to SPIR-V vectors (including many integer and float operations // which work on them), so simply use those. // Note: SPIR-V vectors only support bools, ints and floats, so pointer vectors need to be supported another way. @@ -532,10 +1195,112 @@ pub const DeclGen = struct { const payload = try self.spv.arena.create(SpvType.Payload.Vector); payload.* = .{ - .component_type = try self.resolveType(ty.elemType()), + .component_type = try self.resolveType(ty.elemType(), repr), .component_count = @intCast(u32, ty.vectorLen()), }; - break :blk try self.spv.resolveType(SpvType.initPayload(&payload.base)); + return try self.spv.resolveType(SpvType.initPayload(&payload.base)); + }, + .Struct => { + if (ty.isSimpleTupleOrAnonStruct()) { + const tuple = ty.tupleFields(); + const members = try self.spv.arena.alloc(SpvType.Payload.Struct.Member, tuple.types.len); + var member_index: u32 = 0; + for (tuple.types, 0..) |field_ty, i| { + const field_val = tuple.values[i]; + if (field_val.tag() != .unreachable_value or !field_ty.hasRuntimeBitsIgnoreComptime()) continue; + members[member_index] = .{ + .ty = try self.resolveType(field_ty, .indirect), + }; + member_index += 1; + } + const payload = try self.spv.arena.create(SpvType.Payload.Struct); + payload.* = .{ + .members = members[0..member_index], + }; + return try self.spv.resolveType(SpvType.initPayload(&payload.base)); + } + + const struct_ty = ty.castTag(.@"struct").?.data; + + if (struct_ty.layout == .Packed) { + return try self.resolveType(struct_ty.backing_int_ty, .indirect); + } + + const members = try self.spv.arena.alloc(SpvType.Payload.Struct.Member, struct_ty.fields.count()); + var member_index: usize = 0; + for (struct_ty.fields.values(), 0..) |field, i| { + if (field.is_comptime or !field.ty.hasRuntimeBits()) continue; + + members[member_index] = .{ + .ty = try self.resolveType(field.ty, .indirect), + .name = struct_ty.fields.keys()[i], + }; + member_index += 1; + } + + const name = try struct_ty.getFullyQualifiedName(self.module); + defer self.module.gpa.free(name); + + const payload = try self.spv.arena.create(SpvType.Payload.Struct); + payload.* = .{ + .members = members[0..member_index], + .name = try self.spv.arena.dupe(u8, name), + }; + return try self.spv.resolveType(SpvType.initPayload(&payload.base)); + }, + .Optional => { + var buf: Type.Payload.ElemType = undefined; + const payload_ty = ty.optionalChild(&buf); + if (!payload_ty.hasRuntimeBitsIgnoreComptime()) { + // Just use a bool. + // Note: Always generate the bool with indirect format, to save on some sanity + // Perform the converison to a direct bool when the field is extracted. + return try self.resolveType(Type.bool, .indirect); + } + + const payload_ty_ref = try self.resolveType(payload_ty, .indirect); + if (ty.optionalReprIsPayload()) { + // Optional is actually a pointer. + return payload_ty_ref; + } + + const bool_ty_ref = try self.resolveType(Type.bool, .indirect); + + // its an actual optional + return try self.spv.simpleStructType(&.{ + .{ .ty = payload_ty_ref, .name = "payload" }, + .{ .ty = bool_ty_ref, .name = "valid" }, + }); + }, + .Union => return try self.resolveUnionType(ty, null), + .ErrorSet => return try self.intType(.unsigned, 16), + .ErrorUnion => { + const payload_ty = ty.errorUnionPayload(); + const error_ty_ref = try self.resolveType(Type.anyerror, .indirect); + if (!payload_ty.hasRuntimeBitsIgnoreComptime()) { + return error_ty_ref; + } + + const payload_ty_ref = try self.resolveType(payload_ty, .indirect); + + const payload_align = payload_ty.abiAlignment(target); + const error_align = Type.anyerror.abiAlignment(target); + + var members = std.BoundedArray(SpvType.Payload.Struct.Member, 2){}; + // Similar to unions, we're going to put the most aligned member first. + if (error_align > payload_align) { + // Put the error first + members.appendAssumeCapacity(.{ .ty = error_ty_ref, .name = "error" }); + members.appendAssumeCapacity(.{ .ty = payload_ty_ref, .name = "payload" }); + // TODO: ABI padding? + } else { + // Put the payload first. + members.appendAssumeCapacity(.{ .ty = payload_ty_ref, .name = "payload" }); + members.appendAssumeCapacity(.{ .ty = error_ty_ref, .name = "error" }); + // TODO: ABI padding? + } + + return try self.spv.simpleStructType(members.slice()); }, .Null, @@ -547,31 +1312,120 @@ pub const DeclGen = struct { => unreachable, // Must be comptime. else => |tag| return self.todo("Implement zig type '{}'", .{tag}), + } + } + + fn spvStorageClass(as: std.builtin.AddressSpace) StorageClass { + return switch (as) { + .generic => .Generic, + .shared => .Workgroup, + .local => .Private, + .global => .CrossWorkgroup, + .constant => .UniformConstant, + .gs, + .fs, + .ss, + .param, + .flash, + .flash1, + .flash2, + .flash3, + .flash4, + .flash5, + => unreachable, }; } - fn spirvStorageClass(as: std.builtin.AddressSpace) spec.StorageClass { - return switch (as) { - .generic => .Generic, // TODO: Disallow? - .gs, .fs, .ss => unreachable, - .shared => .Workgroup, - .local => .Private, - .global, .param, .constant, .flash, .flash1, .flash2, .flash3, .flash4, .flash5 => unreachable, + /// The SPIR-V backend is not yet advanced enough to support the std testing infrastructure. + /// In order to be able to run tests, we "temporarily" lower test kernels into separate entry- + /// points. The test executor will then be able to invoke these to run the tests. + /// Note that tests are lowered according to std.builtin.TestFn, which is `fn () anyerror!void`. + /// (anyerror!void has the same layout as anyerror). + /// Each test declaration generates a function like. + /// %anyerror = OpTypeInt 0 16 + /// %p_anyerror = OpTypePointer CrossWorkgroup %anyerror + /// %K = OpTypeFunction %void %p_anyerror + /// + /// %test = OpFunction %void %K + /// %p_err = OpFunctionParameter %p_anyerror + /// %lbl = OpLabel + /// %result = OpFunctionCall %anyerror %func + /// OpStore %p_err %result + /// OpFunctionEnd + /// TODO is to also write out the error as a function call parameter, and to somehow fetch + /// the name of an error in the text executor. + fn generateTestEntryPoint(self: *DeclGen, name: []const u8, spv_test_decl_index: SpvModule.Decl.Index) !void { + const anyerror_ty_ref = try self.resolveType(Type.anyerror, .direct); + const ptr_anyerror_ty_ref = try self.spv.ptrType(anyerror_ty_ref, .CrossWorkgroup, 0); + const void_ty_ref = try self.resolveType(Type.void, .direct); + + const kernel_proto_ty_ref = blk: { + const proto_payload = try self.spv.arena.create(SpvType.Payload.Function); + proto_payload.* = .{ + .return_type = void_ty_ref, + .parameters = try self.spv.arena.dupe(SpvType.Ref, &.{ptr_anyerror_ty_ref}), + }; + break :blk try self.spv.resolveType(SpvType.initPayload(&proto_payload.base)); }; + + const test_id = self.spv.declPtr(spv_test_decl_index).result_id; + + const spv_decl_index = try self.spv.allocDecl(.func); + const kernel_id = self.spv.declPtr(spv_decl_index).result_id; + + const error_id = self.spv.allocId(); + const p_error_id = self.spv.allocId(); + + const section = &self.spv.sections.functions; + try section.emit(self.spv.gpa, .OpFunction, .{ + .id_result_type = self.typeId(void_ty_ref), + .id_result = kernel_id, + .function_control = .{}, + .function_type = self.typeId(kernel_proto_ty_ref), + }); + try section.emit(self.spv.gpa, .OpFunctionParameter, .{ + .id_result_type = self.typeId(ptr_anyerror_ty_ref), + .id_result = p_error_id, + }); + try section.emit(self.spv.gpa, .OpLabel, .{ + .id_result = self.spv.allocId(), + }); + try section.emit(self.spv.gpa, .OpFunctionCall, .{ + .id_result_type = self.typeId(anyerror_ty_ref), + .id_result = error_id, + .function = test_id, + }); + try section.emit(self.spv.gpa, .OpStore, .{ + .pointer = p_error_id, + .object = error_id, + }); + try section.emit(self.spv.gpa, .OpReturn, {}); + try section.emit(self.spv.gpa, .OpFunctionEnd, {}); + + try self.spv.declareDeclDeps(spv_decl_index, &.{spv_test_decl_index}); + + // Just generate a quick other name because the intel runtime crashes when the entry- + // point name is the same as a different OpName. + const test_name = try std.fmt.allocPrint(self.gpa, "test {s}", .{name}); + defer self.gpa.free(test_name); + try self.spv.declareEntryPoint(spv_decl_index, test_name); } fn genDecl(self: *DeclGen) !void { - const result_id = self.ids.get(self.decl_index).?; const decl = self.module.declPtr(self.decl_index); + const spv_decl_index = try self.resolveDecl(self.decl_index); + + const decl_id = self.spv.declPtr(spv_decl_index).result_id; + log.debug("genDecl {s} = {}", .{ decl.name, decl_id }); if (decl.val.castTag(.function)) |_| { assert(decl.ty.zigTypeTag() == .Fn); const prototype_id = try self.resolveTypeId(decl.ty); try self.func.prologue.emit(self.spv.gpa, .OpFunction, .{ .id_result_type = try self.resolveTypeId(decl.ty.fnReturnType()), - .id_result = result_id, + .id_result = decl_id, .function_control = .{}, // TODO: We can set inline here if the type requires it. - .function_type = prototype_id.toRef(), + .function_type = prototype_id, }); const params = decl.ty.fnParamLen(); @@ -585,7 +1439,7 @@ pub const DeclGen = struct { .id_result_type = param_type_id, .id_result = arg_result_id, }); - self.args.appendAssumeCapacity(arg_result_id.toRef()); + self.args.appendAssumeCapacity(arg_result_id); } // TODO: This could probably be done in a better way... @@ -596,17 +1450,53 @@ pub const DeclGen = struct { try self.func.prologue.emit(self.spv.gpa, .OpLabel, .{ .id_result = root_block_id, }); - self.current_block_label_id = root_block_id.toRef(); + self.current_block_label_id = root_block_id; const main_body = self.air.getMainBody(); try self.genBody(main_body); // Append the actual code into the functions section. try self.func.body.emit(self.spv.gpa, .OpFunctionEnd, {}); - try self.spv.addFunction(self.func); + try self.spv.addFunction(spv_decl_index, self.func); + + const fqn = try decl.getFullyQualifiedName(self.module); + defer self.module.gpa.free(fqn); + + try self.spv.sections.debug_names.emit(self.gpa, .OpName, .{ + .target = decl_id, + .name = fqn, + }); + + // Temporarily generate a test kernel declaration if this is a test function. + if (self.module.test_functions.contains(self.decl_index)) { + try self.generateTestEntryPoint(fqn, spv_decl_index); + } } else { - // TODO - // return self.todo("generate decl type {}", .{decl.ty.zigTypeTag()}); + const init_val = if (decl.val.castTag(.variable)) |payload| + payload.data.init + else + decl.val; + + if (init_val.tag() == .unreachable_value) { + return self.todo("importing extern variables", .{}); + } + + // TODO: integrate with variable(). + + const final_storage_class = spvStorageClass(decl.@"addrspace"); + const actual_storage_class = switch (final_storage_class) { + .Generic => .CrossWorkgroup, + else => final_storage_class, + }; + + try self.lowerIndirectConstant( + spv_decl_index, + decl.ty, + init_val, + actual_storage_class, + final_storage_class == .Generic, + decl.@"align", + ); } } @@ -618,11 +1508,25 @@ pub const DeclGen = struct { fn genInst(self: *DeclGen, inst: Air.Inst.Index) !void { const air_tags = self.air.instructions.items(.tag); - const result_id = switch (air_tags[inst]) { + const maybe_result_id: ?IdRef = switch (air_tags[inst]) { // zig fmt: off - .add, .addwrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd), - .sub, .subwrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub), - .mul, .mulwrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul), + .add, .addwrap => try self.airArithOp(inst, .OpFAdd, .OpIAdd, .OpIAdd, true), + .sub, .subwrap => try self.airArithOp(inst, .OpFSub, .OpISub, .OpISub, true), + .mul, .mulwrap => try self.airArithOp(inst, .OpFMul, .OpIMul, .OpIMul, true), + + .div_float, + .div_float_optimized, + // TODO: Check that this is the right operation. + .div_trunc, + .div_trunc_optimized, + => try self.airArithOp(inst, .OpFDiv, .OpSDiv, .OpUDiv, false), + // TODO: Check if this is the right operation + // TODO: Make airArithOp for rem not emit a mask for the LHS. + .rem, + .rem_optimized, + => try self.airArithOp(inst, .OpFRem, .OpSRem, .OpSRem, false), + + .add_with_overflow => try self.airOverflowArithOp(inst), .shuffle => try self.airShuffle(inst), @@ -632,7 +1536,24 @@ pub const DeclGen = struct { .bool_and => try self.airBinOpSimple(inst, .OpLogicalAnd), .bool_or => try self.airBinOpSimple(inst, .OpLogicalOr), - .not => try self.airNot(inst), + .shl => try self.airShift(inst, .OpShiftLeftLogical), + + .bitcast => try self.airBitcast(inst), + .intcast => try self.airIntcast(inst), + .not => try self.airNot(inst), + + .slice_ptr => try self.airSliceField(inst, 0), + .slice_len => try self.airSliceField(inst, 1), + .slice_elem_ptr => try self.airSliceElemPtr(inst), + .slice_elem_val => try self.airSliceElemVal(inst), + .ptr_elem_ptr => try self.airPtrElemPtr(inst), + + .struct_field_val => try self.airStructFieldVal(inst), + + .struct_field_ptr_index_0 => try self.airStructFieldPtrIndex(inst, 0), + .struct_field_ptr_index_1 => try self.airStructFieldPtrIndex(inst, 1), + .struct_field_ptr_index_2 => try self.airStructFieldPtrIndex(inst, 2), + .struct_field_ptr_index_3 => try self.airStructFieldPtrIndex(inst, 3), .cmp_eq => try self.airCmp(inst, .OpFOrdEqual, .OpLogicalEqual, .OpIEqual), .cmp_neq => try self.airCmp(inst, .OpFOrdNotEqual, .OpLogicalNotEqual, .OpINotEqual), @@ -641,21 +1562,33 @@ pub const DeclGen = struct { .cmp_lt => try self.airCmp(inst, .OpFOrdLessThan, .OpSLessThan, .OpULessThan), .cmp_lte => try self.airCmp(inst, .OpFOrdLessThanEqual, .OpSLessThanEqual, .OpULessThanEqual), - .arg => self.airArg(), - .alloc => try self.airAlloc(inst), - .block => (try self.airBlock(inst)) orelse return, - .load => try self.airLoad(inst), + .arg => self.airArg(), + .alloc => try self.airAlloc(inst), + // TODO: We probably need to have a special implementation of this for the C abi. + .ret_ptr => try self.airAlloc(inst), + .block => try self.airBlock(inst), + + .load => try self.airLoad(inst), + .store => return self.airStore(inst), .br => return self.airBr(inst), .breakpoint => return, .cond_br => return self.airCondBr(inst), .constant => unreachable, + .const_ty => unreachable, .dbg_stmt => return self.airDbgStmt(inst), .loop => return self.airLoop(inst), .ret => return self.airRet(inst), - .store => return self.airStore(inst), + .ret_load => return self.airRetLoad(inst), + .switch_br => return self.airSwitchBr(inst), .unreach => return self.airUnreach(), - .assembly => (try self.airAssembly(inst)) orelse return, + + .assembly => try self.airAssembly(inst), + + .call => try self.airCall(inst, .auto), + .call_always_tail => try self.airCall(inst, .always_tail), + .call_never_tail => try self.airCall(inst, .never_tail), + .call_never_inline => try self.airCall(inst, .never_inline), .dbg_var_ptr => return, .dbg_var_val => return, @@ -666,10 +1599,12 @@ pub const DeclGen = struct { else => |tag| return self.todo("implement AIR tag {s}", .{@tagName(tag)}), }; + const result_id = maybe_result_id orelse return; try self.inst_results.putNoClobber(self.gpa, inst, result_id); } - fn airBinOpSimple(self: *DeclGen, inst: Air.Inst.Index, comptime opcode: Opcode) !IdRef { + fn airBinOpSimple(self: *DeclGen, inst: Air.Inst.Index, comptime opcode: Opcode) !?IdRef { + if (self.liveness.isUnused(inst)) return null; const bin_op = self.air.instructions.items(.data)[inst].bin_op; const lhs_id = try self.resolve(bin_op.lhs); const rhs_id = try self.resolve(bin_op.rhs); @@ -681,7 +1616,45 @@ pub const DeclGen = struct { .operand_1 = lhs_id, .operand_2 = rhs_id, }); - return result_id.toRef(); + return result_id; + } + + fn airShift(self: *DeclGen, inst: Air.Inst.Index, comptime opcode: Opcode) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const lhs_id = try self.resolve(bin_op.lhs); + const rhs_id = try self.resolve(bin_op.rhs); + const result_type_id = try self.resolveTypeId(self.air.typeOfIndex(inst)); + + // the shift and the base must be the same type in SPIR-V, but in Zig the shift is a smaller int. + const shift_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpUConvert, .{ + .id_result_type = result_type_id, + .id_result = shift_id, + .unsigned_value = rhs_id, + }); + + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, opcode, .{ + .id_result_type = result_type_id, + .id_result = result_id, + .base = lhs_id, + .shift = shift_id, + }); + return result_id; + } + + fn maskStrangeInt(self: *DeclGen, ty_ref: SpvType.Ref, value_id: IdRef, bits: u16) !IdRef { + const mask_value = if (bits == 64) 0xFFFF_FFFF_FFFF_FFFF else (@as(u64, 1) << @intCast(u6, bits)) - 1; + const result_id = self.spv.allocId(); + const mask_id = try self.constInt(ty_ref, mask_value); + try self.func.body.emit(self.spv.gpa, .OpBitwiseAnd, .{ + .id_result_type = self.typeId(ty_ref), + .id_result = result_id, + .operand_1 = value_id, + .operand_2 = mask_id, + }); + return result_id; } fn airArithOp( @@ -690,16 +1663,18 @@ pub const DeclGen = struct { comptime fop: Opcode, comptime sop: Opcode, comptime uop: Opcode, - ) !IdRef { + /// true if this operation holds under modular arithmetic. + comptime modular: bool, + ) !?IdRef { + if (self.liveness.isUnused(inst)) return null; // LHS and RHS are guaranteed to have the same type, and AIR guarantees // the result to be the same as the LHS and RHS, which matches SPIR-V. const ty = self.air.typeOfIndex(inst); const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const lhs_id = try self.resolve(bin_op.lhs); - const rhs_id = try self.resolve(bin_op.rhs); + var lhs_id = try self.resolve(bin_op.lhs); + var rhs_id = try self.resolve(bin_op.rhs); - const result_id = self.spv.allocId(); - const result_type_id = try self.resolveTypeId(ty); + const result_ty_ref = try self.resolveType(ty, .direct); assert(self.air.typeOf(bin_op.lhs).eql(ty, self.module)); assert(self.air.typeOf(bin_op.rhs).eql(ty, self.module)); @@ -712,19 +1687,27 @@ pub const DeclGen = struct { .composite_integer => { return self.todo("binary operations for composite integers", .{}); }, - .strange_integer => { - return self.todo("binary operations for strange integers", .{}); + .strange_integer => blk: { + if (!modular) { + lhs_id = try self.maskStrangeInt(result_ty_ref, lhs_id, info.bits); + rhs_id = try self.maskStrangeInt(result_ty_ref, rhs_id, info.bits); + } + break :blk switch (info.signedness) { + .signed => @as(usize, 1), + .unsigned => @as(usize, 2), + }; }, .integer => switch (info.signedness) { .signed => @as(usize, 1), .unsigned => @as(usize, 2), }, .float => 0, - else => unreachable, + .bool => unreachable, }; + const result_id = self.spv.allocId(); const operands = .{ - .id_result_type = result_type_id, + .id_result_type = self.typeId(result_ty_ref), .id_result = result_id, .operand_1 = lhs_id, .operand_2 = rhs_id, @@ -739,10 +1722,90 @@ pub const DeclGen = struct { // TODO: Trap on overflow? Probably going to be annoying. // TODO: Look into SPV_KHR_no_integer_wrap_decoration which provides NoSignedWrap/NoUnsignedWrap. - return result_id.toRef(); + return result_id; } - fn airShuffle(self: *DeclGen, inst: Air.Inst.Index) !IdRef { + fn airOverflowArithOp(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.Bin, ty_pl.payload).data; + const lhs = try self.resolve(extra.lhs); + const rhs = try self.resolve(extra.rhs); + + const operand_ty = self.air.typeOf(extra.lhs); + const result_ty = self.air.typeOfIndex(inst); + + const info = try self.arithmeticTypeInfo(operand_ty); + switch (info.class) { + .composite_integer => return self.todo("overflow ops for composite integers", .{}), + .strange_integer => return self.todo("overflow ops for strange integers", .{}), + .integer => {}, + .float, .bool => unreachable, + } + + const operand_ty_id = try self.resolveTypeId(operand_ty); + const result_type_id = try self.resolveTypeId(result_ty); + + const overflow_member_ty = try self.intType(.unsigned, info.bits); + const overflow_member_ty_id = self.typeId(overflow_member_ty); + + const op_result_id = blk: { + // Construct the SPIR-V result type. + // It is almost the same as the zig one, except that the fields must be the same type + // and they must be unsigned. + const overflow_result_ty_ref = try self.spv.simpleStructType(&.{ + .{ .ty = overflow_member_ty, .name = "res" }, + .{ .ty = overflow_member_ty, .name = "ov" }, + }); + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpIAddCarry, .{ + .id_result_type = self.typeId(overflow_result_ty_ref), + .id_result = result_id, + .operand_1 = lhs, + .operand_2 = rhs, + }); + break :blk result_id; + }; + + // Now convert the SPIR-V flavor result into a Zig-flavor result. + // First, extract the two fields. + const unsigned_result = try self.extractField(overflow_member_ty_id, op_result_id, 0); + const overflow = try self.extractField(overflow_member_ty_id, op_result_id, 1); + + // We need to convert the results to the types that Zig expects here. + // The `result` is the same type except unsigned, so we can just bitcast that. + const result = try self.bitcast(operand_ty_id, unsigned_result); + + // The overflow needs to be converted into whatever is used to represent it in Zig. + const casted_overflow = blk: { + const ov_ty = result_ty.tupleFields().types[1]; + const ov_ty_id = try self.resolveTypeId(ov_ty); + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpUConvert, .{ + .id_result_type = ov_ty_id, + .id_result = result_id, + .unsigned_value = overflow, + }); + break :blk result_id; + }; + + // TODO: If copying this function for borrow, make sure to convert -1 to 1 as appropriate. + + // Finally, construct the Zig type. + // Layout is result, overflow. + const result_id = self.spv.allocId(); + const constituents = [_]IdRef{ result, casted_overflow }; + try self.func.body.emit(self.spv.gpa, .OpCompositeConstruct, .{ + .id_result_type = result_type_id, + .id_result = result_id, + .constituents = &constituents, + }); + return result_id; + } + + fn airShuffle(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; const ty = self.air.typeOfIndex(inst); const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; const extra = self.air.extraData(Air.Shuffle, ty_pl.payload).data; @@ -774,15 +1837,16 @@ pub const DeclGen = struct { self.func.body.writeOperand(spec.LiteralInteger, unsigned); } } - return result_id.toRef(); + return result_id; } - fn airCmp(self: *DeclGen, inst: Air.Inst.Index, comptime fop: Opcode, comptime sop: Opcode, comptime uop: Opcode) !IdRef { + fn airCmp(self: *DeclGen, inst: Air.Inst.Index, comptime fop: Opcode, comptime sop: Opcode, comptime uop: Opcode) !?IdRef { + if (self.liveness.isUnused(inst)) return null; const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const lhs_id = try self.resolve(bin_op.lhs); - const rhs_id = try self.resolve(bin_op.rhs); + var lhs_id = try self.resolve(bin_op.lhs); + var rhs_id = try self.resolve(bin_op.rhs); const result_id = self.spv.allocId(); - const result_type_id = try self.resolveTypeId(Type.initTag(.bool)); + const result_type_id = try self.resolveTypeId(Type.bool); const op_ty = self.air.typeOf(bin_op.lhs); assert(op_ty.eql(self.air.typeOf(bin_op.rhs), self.module)); @@ -794,11 +1858,17 @@ pub const DeclGen = struct { .composite_integer => { return self.todo("binary operations for composite integers", .{}); }, - .strange_integer => { - return self.todo("comparison for strange integers", .{}); - }, .float => 0, .bool => 1, + .strange_integer => blk: { + const op_ty_ref = try self.resolveType(op_ty, .direct); + lhs_id = try self.maskStrangeInt(op_ty_ref, lhs_id, info.bits); + rhs_id = try self.maskStrangeInt(op_ty_ref, rhs_id, info.bits); + break :blk switch (info.signedness) { + .signed => @as(usize, 1), + .unsigned => @as(usize, 2), + }; + }, .integer => switch (info.signedness) { .signed => @as(usize, 1), .unsigned => @as(usize, 2), @@ -819,41 +1889,336 @@ pub const DeclGen = struct { else => unreachable, } - return result_id.toRef(); + return result_id; } - fn airNot(self: *DeclGen, inst: Air.Inst.Index) !IdRef { + fn bitcast(self: *DeclGen, target_type_id: IdResultType, value_id: IdRef) !IdRef { + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpBitcast, .{ + .id_result_type = target_type_id, + .id_result = result_id, + .operand = value_id, + }); + return result_id; + } + + fn airBitcast(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const operand_id = try self.resolve(ty_op.operand); + const result_type_id = try self.resolveTypeId(self.air.typeOfIndex(inst)); + return try self.bitcast(result_type_id, operand_id); + } + + fn airIntcast(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const operand_id = try self.resolve(ty_op.operand); + const dest_ty = self.air.typeOfIndex(inst); + const dest_info = try self.arithmeticTypeInfo(dest_ty); + const dest_ty_id = try self.resolveTypeId(dest_ty); + + const result_id = self.spv.allocId(); + switch (dest_info.signedness) { + .signed => try self.func.body.emit(self.spv.gpa, .OpSConvert, .{ + .id_result_type = dest_ty_id, + .id_result = result_id, + .signed_value = operand_id, + }), + .unsigned => try self.func.body.emit(self.spv.gpa, .OpUConvert, .{ + .id_result_type = dest_ty_id, + .id_result = result_id, + .unsigned_value = operand_id, + }), + } + return result_id; + } + + fn airNot(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; const ty_op = self.air.instructions.items(.data)[inst].ty_op; const operand_id = try self.resolve(ty_op.operand); const result_id = self.spv.allocId(); - const result_type_id = try self.resolveTypeId(Type.initTag(.bool)); + const result_type_id = try self.resolveTypeId(Type.bool); try self.func.body.emit(self.spv.gpa, .OpLogicalNot, .{ .id_result_type = result_type_id, .id_result = result_id, .operand = operand_id, }); - return result_id.toRef(); + return result_id; } - fn airAlloc(self: *DeclGen, inst: Air.Inst.Index) !IdRef { - const ty = self.air.typeOfIndex(inst); - const result_type_id = try self.resolveTypeId(ty); + fn extractField(self: *DeclGen, result_ty: IdResultType, object: IdRef, field: u32) !IdRef { const result_id = self.spv.allocId(); + const indexes = [_]u32{field}; + try self.func.body.emit(self.spv.gpa, .OpCompositeExtract, .{ + .id_result_type = result_ty, + .id_result = result_id, + .composite = object, + .indexes = &indexes, + }); + return result_id; + } - // Rather than generating into code here, we're just going to generate directly into the functions section so that - // variable declarations appear in the first block of the function. - const storage_class = spirvStorageClass(ty.ptrAddressSpace()); - const section = if (storage_class == .Function) - &self.func.prologue - else - &self.spv.sections.types_globals_constants; + fn airSliceField(self: *DeclGen, inst: Air.Inst.Index, field: u32) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + return try self.extractField( + try self.resolveTypeId(self.air.typeOfIndex(inst)), + try self.resolve(ty_op.operand), + field, + ); + } - try section.emit(self.spv.gpa, .OpVariable, .{ + fn airSliceElemPtr(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const slice_ty = self.air.typeOf(bin_op.lhs); + if (!slice_ty.isVolatilePtr() and self.liveness.isUnused(inst)) return null; + + const slice = try self.resolve(bin_op.lhs); + const index = try self.resolve(bin_op.rhs); + + const spv_ptr_ty = try self.resolveTypeId(self.air.typeOfIndex(inst)); + + const slice_ptr = blk: { + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpCompositeExtract, .{ + .id_result_type = spv_ptr_ty, + .id_result = result_id, + .composite = slice, + .indexes = &.{0}, + }); + break :blk result_id; + }; + + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpInBoundsPtrAccessChain, .{ + .id_result_type = spv_ptr_ty, + .id_result = result_id, + .base = slice_ptr, + .element = index, + }); + return result_id; + } + + fn airSliceElemVal(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const slice_ty = self.air.typeOf(bin_op.lhs); + if (!slice_ty.isVolatilePtr() and self.liveness.isUnused(inst)) return null; + + const slice = try self.resolve(bin_op.lhs); + const index = try self.resolve(bin_op.rhs); + + var slice_buf: Type.SlicePtrFieldTypeBuffer = undefined; + const ptr_ty_id = try self.resolveTypeId(slice_ty.slicePtrFieldType(&slice_buf)); + + const slice_ptr = blk: { + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpCompositeExtract, .{ + .id_result_type = ptr_ty_id, + .id_result = result_id, + .composite = slice, + .indexes = &.{0}, + }); + break :blk result_id; + }; + + const elem_ptr = blk: { + const result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpInBoundsPtrAccessChain, .{ + .id_result_type = ptr_ty_id, + .id_result = result_id, + .base = slice_ptr, + .element = index, + }); + break :blk result_id; + }; + + return try self.load(slice_ty, elem_ptr); + } + + fn airPtrElemPtr(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const bin_op = self.air.extraData(Air.Bin, ty_pl.payload).data; + const ptr_ty = self.air.typeOf(bin_op.lhs); + const result_ty = self.air.typeOfIndex(inst); + const elem_ty = ptr_ty.childType(); + // TODO: Make this return a null ptr or something + if (!elem_ty.hasRuntimeBitsIgnoreComptime()) return null; + + const result_type_id = try self.resolveTypeId(result_ty); + const base_ptr = try self.resolve(bin_op.lhs); + const rhs = try self.resolve(bin_op.rhs); + + const result_id = self.spv.allocId(); + const indexes = [_]IdRef{rhs}; + try self.func.body.emit(self.spv.gpa, .OpInBoundsAccessChain, .{ .id_result_type = result_type_id, .id_result = result_id, - .storage_class = storage_class, + .base = base_ptr, + .indexes = &indexes, }); - return result_id.toRef(); + return result_id; + } + + fn airStructFieldVal(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const struct_field = self.air.extraData(Air.StructField, ty_pl.payload).data; + + const struct_ty = self.air.typeOf(struct_field.struct_operand); + const object = try self.resolve(struct_field.struct_operand); + const field_index = struct_field.field_index; + const field_ty = struct_ty.structFieldType(field_index); + const field_ty_id = try self.resolveTypeId(field_ty); + + if (!field_ty.hasRuntimeBitsIgnoreComptime()) return null; + + assert(struct_ty.zigTypeTag() == .Struct); // Cannot do unions yet. + + const result_id = self.spv.allocId(); + const indexes = [_]u32{field_index}; + try self.func.body.emit(self.spv.gpa, .OpCompositeExtract, .{ + .id_result_type = field_ty_id, + .id_result = result_id, + .composite = object, + .indexes = &indexes, + }); + return result_id; + } + + fn structFieldPtr( + self: *DeclGen, + result_ptr_ty: Type, + object_ptr_ty: Type, + object_ptr: IdRef, + field_index: u32, + ) !?IdRef { + const object_ty = object_ptr_ty.childType(); + switch (object_ty.zigTypeTag()) { + .Struct => switch (object_ty.containerLayout()) { + .Packed => unreachable, // TODO + else => { + const u32_ty_id = self.typeId(try self.intType(.unsigned, 32)); + const field_index_id = self.spv.allocId(); + try self.spv.emitConstant(u32_ty_id, field_index_id, .{ .uint32 = field_index }); + const result_id = self.spv.allocId(); + const result_type_id = try self.resolveTypeId(result_ptr_ty); + const indexes = [_]IdRef{field_index_id}; + try self.func.body.emit(self.spv.gpa, .OpInBoundsAccessChain, .{ + .id_result_type = result_type_id, + .id_result = result_id, + .base = object_ptr, + .indexes = &indexes, + }); + return result_id; + }, + }, + else => unreachable, // TODO + } + } + + fn airStructFieldPtrIndex(self: *DeclGen, inst: Air.Inst.Index, field_index: u32) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const struct_ptr = try self.resolve(ty_op.operand); + const struct_ptr_ty = self.air.typeOf(ty_op.operand); + const result_ptr_ty = self.air.typeOfIndex(inst); + return try self.structFieldPtr(result_ptr_ty, struct_ptr_ty, struct_ptr, field_index); + } + + /// We cannot use an OpVariable directly in an OpSpecConstantOp, but we can + /// after we insert a dummy AccessChain... + /// TODO: Get rid of this + fn makePointerConstant( + self: *DeclGen, + section: *SpvSection, + ptr_ty_ref: SpvType.Ref, + ptr_id: IdRef, + ) !IdRef { + const result_id = self.spv.allocId(); + try section.emitSpecConstantOp(self.spv.gpa, .OpInBoundsAccessChain, .{ + .id_result_type = self.typeId(ptr_ty_ref), + .id_result = result_id, + .base = ptr_id, + }); + return result_id; + } + + fn variable( + self: *DeclGen, + comptime context: enum { function, global }, + result_id: IdRef, + ptr_ty_ref: SpvType.Ref, + initializer: ?IdRef, + ) !void { + const storage_class = self.spv.typeRefType(ptr_ty_ref).payload(.pointer).storage_class; + const actual_storage_class = switch (storage_class) { + .Generic => switch (context) { + .function => .Function, + .global => .CrossWorkgroup, + }, + else => storage_class, + }; + const actual_ptr_ty_ref = switch (storage_class) { + .Generic => try self.spv.changePtrStorageClass(ptr_ty_ref, actual_storage_class), + else => ptr_ty_ref, + }; + const alloc_result_id = switch (storage_class) { + .Generic => self.spv.allocId(), + else => result_id, + }; + + const section = switch (actual_storage_class) { + .Generic => unreachable, + // SPIR-V requires that OpVariable declarations for locals go into the first block, so we are just going to + // directly generate them into func.prologue instead of the body. + .Function => &self.func.prologue, + else => &self.spv.sections.types_globals_constants, + }; + try section.emit(self.spv.gpa, .OpVariable, .{ + .id_result_type = self.typeId(actual_ptr_ty_ref), + .id_result = alloc_result_id, + .storage_class = actual_storage_class, + .initializer = initializer, + }); + + if (storage_class != .Generic) { + return; + } + + // Now we need to convert the pointer. + // If this is a function local, we need to perform the conversion at runtime. Otherwise, we can do + // it ahead of time using OpSpecConstantOp. + switch (actual_storage_class) { + .Function => try self.func.body.emit(self.spv.gpa, .OpPtrCastToGeneric, .{ + .id_result_type = self.typeId(ptr_ty_ref), + .id_result = result_id, + .pointer = alloc_result_id, + }), + // TODO: Can we do without this cast or move it to runtime? + else => { + const const_ptr_id = try self.makePointerConstant(section, actual_ptr_ty_ref, alloc_result_id); + try section.emitSpecConstantOp(self.spv.gpa, .OpPtrCastToGeneric, .{ + .id_result_type = self.typeId(ptr_ty_ref), + .id_result = result_id, + .pointer = const_ptr_id, + }); + }, + } + } + + fn airAlloc(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { + if (self.liveness.isUnused(inst)) return null; + const ty = self.air.typeOfIndex(inst); + const result_ty_ref = try self.resolveType(ty, .direct); + const result_id = self.spv.allocId(); + try self.variable(.function, result_id, result_ty_ref, null); + return result_id; } fn airArg(self: *DeclGen) IdRef { @@ -873,7 +2238,7 @@ pub const DeclGen = struct { var incoming_blocks = try std.ArrayListUnmanaged(IncomingBlock).initCapacity(self.gpa, 4); try self.blocks.putNoClobber(self.gpa, inst, .{ - .label_id = label_id.toRef(), + .label_id = label_id, .incoming_blocks = &incoming_blocks, }); defer { @@ -890,7 +2255,7 @@ pub const DeclGen = struct { try self.beginSpvBlock(label_id); // If this block didn't produce a value, simply return here. - if (!ty.hasRuntimeBits()) + if (!ty.hasRuntimeBitsIgnoreComptime()) return null; // Combine the result from the blocks using the Phi instruction. @@ -908,7 +2273,7 @@ pub const DeclGen = struct { self.func.body.writeOperand(spec.PairIdRefIdRef, .{ incoming.break_value_id, incoming.src_label_id }); } - return result_id.toRef(); + return result_id; } fn airBr(self: *DeclGen, inst: Air.Inst.Index) !void { @@ -941,8 +2306,8 @@ pub const DeclGen = struct { try self.func.body.emit(self.spv.gpa, .OpBranchConditional, .{ .condition = condition_id, - .true_label = then_label_id.toRef(), - .false_label = else_label_id.toRef(), + .true_label = then_label_id, + .false_label = else_label_id, }); try self.beginSpvBlock(then_label_id); @@ -961,26 +2326,80 @@ pub const DeclGen = struct { }); } - fn airLoad(self: *DeclGen, inst: Air.Inst.Index) !IdRef { + fn airLoad(self: *DeclGen, inst: Air.Inst.Index) !?IdRef { const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const operand_id = try self.resolve(ty_op.operand); - const ty = self.air.typeOfIndex(inst); + const ptr_ty = self.air.typeOf(ty_op.operand); + const operand = try self.resolve(ty_op.operand); + if (!ptr_ty.isVolatilePtr() and self.liveness.isUnused(inst)) return null; - const result_type_id = try self.resolveTypeId(ty); + return try self.load(ptr_ty, operand); + } + + fn load(self: *DeclGen, ptr_ty: Type, ptr: IdRef) !IdRef { + const value_ty = ptr_ty.childType(); + const direct_result_ty_ref = try self.resolveType(value_ty, .direct); + const indirect_result_ty_ref = try self.resolveType(value_ty, .indirect); const result_id = self.spv.allocId(); - const access = spec.MemoryAccess.Extended{ - .Volatile = ty.isVolatilePtr(), + .Volatile = ptr_ty.isVolatilePtr(), }; - try self.func.body.emit(self.spv.gpa, .OpLoad, .{ - .id_result_type = result_type_id, + .id_result_type = self.typeId(indirect_result_ty_ref), .id_result = result_id, - .pointer = operand_id, + .pointer = ptr, .memory_access = access, }); + if (value_ty.zigTypeTag() == .Bool) { + // Convert indirect bool to direct bool + const zero_id = try self.constInt(indirect_result_ty_ref, 0); + const casted_result_id = self.spv.allocId(); + try self.func.body.emit(self.spv.gpa, .OpINotEqual, .{ + .id_result_type = self.typeId(direct_result_ty_ref), + .id_result = casted_result_id, + .operand_1 = result_id, + .operand_2 = zero_id, + }); + return casted_result_id; + } + return result_id; + } - return result_id.toRef(); + fn airStore(self: *DeclGen, inst: Air.Inst.Index) !void { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const ptr_ty = self.air.typeOf(bin_op.lhs); + const ptr = try self.resolve(bin_op.lhs); + const value = try self.resolve(bin_op.rhs); + + try self.store(ptr_ty, ptr, value); + } + + fn store(self: *DeclGen, ptr_ty: Type, ptr: IdRef, value: IdRef) !void { + const value_ty = ptr_ty.childType(); + const converted_value = switch (value_ty.zigTypeTag()) { + .Bool => blk: { + const indirect_bool_ty_ref = try self.resolveType(value_ty, .indirect); + const result_id = self.spv.allocId(); + const zero = try self.constInt(indirect_bool_ty_ref, 0); + const one = try self.constInt(indirect_bool_ty_ref, 1); + try self.func.body.emit(self.spv.gpa, .OpSelect, .{ + .id_result_type = self.typeId(indirect_bool_ty_ref), + .id_result = result_id, + .condition = value, + .object_1 = one, + .object_2 = zero, + }); + break :blk result_id; + }, + else => value, + }; + const access = spec.MemoryAccess.Extended{ + .Volatile = ptr_ty.isVolatilePtr(), + }; + try self.func.body.emit(self.spv.gpa, .OpStore, .{ + .pointer = ptr, + .object = converted_value, + .memory_access = access, + }); } fn airLoop(self: *DeclGen, inst: Air.Inst.Index) !void { @@ -990,13 +2409,13 @@ pub const DeclGen = struct { const loop_label_id = self.spv.allocId(); // Jump to the loop entry point - try self.func.body.emit(self.spv.gpa, .OpBranch, .{ .target_label = loop_label_id.toRef() }); + try self.func.body.emit(self.spv.gpa, .OpBranch, .{ .target_label = loop_label_id }); // TODO: Look into OpLoopMerge. try self.beginSpvBlock(loop_label_id); try self.genBody(body); - try self.func.body.emit(self.spv.gpa, .OpBranch, .{ .target_label = loop_label_id.toRef() }); + try self.func.body.emit(self.spv.gpa, .OpBranch, .{ .target_label = loop_label_id }); } fn airRet(self: *DeclGen, inst: Air.Inst.Index) !void { @@ -1010,21 +2429,136 @@ pub const DeclGen = struct { } } - fn airStore(self: *DeclGen, inst: Air.Inst.Index) !void { - const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const dst_ptr_id = try self.resolve(bin_op.lhs); - const src_val_id = try self.resolve(bin_op.rhs); - const lhs_ty = self.air.typeOf(bin_op.lhs); + fn airRetLoad(self: *DeclGen, inst: Air.Inst.Index) !void { + const un_op = self.air.instructions.items(.data)[inst].un_op; + const ptr_ty = self.air.typeOf(un_op); + const ret_ty = ptr_ty.childType(); - const access = spec.MemoryAccess.Extended{ - .Volatile = lhs_ty.isVolatilePtr(), + if (!ret_ty.hasRuntimeBitsIgnoreComptime()) { + try self.func.body.emit(self.spv.gpa, .OpReturn, {}); + return; + } + + const ptr = try self.resolve(un_op); + const value = try self.load(ptr_ty, ptr); + try self.func.body.emit(self.spv.gpa, .OpReturnValue, .{ + .value = value, + }); + } + + fn airSwitchBr(self: *DeclGen, inst: Air.Inst.Index) !void { + const target = self.getTarget(); + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const cond = try self.resolve(pl_op.operand); + const cond_ty = self.air.typeOf(pl_op.operand); + const switch_br = self.air.extraData(Air.SwitchBr, pl_op.payload); + + const cond_words: u32 = switch (cond_ty.zigTypeTag()) { + .Int => blk: { + const bits = cond_ty.intInfo(target).bits; + const backing_bits = self.backingIntBits(bits) orelse { + return self.todo("implement composite int switch", .{}); + }; + break :blk if (backing_bits <= 32) @as(u32, 1) else 2; + }, + .Enum => blk: { + var buffer: Type.Payload.Bits = undefined; + const int_ty = cond_ty.intTagType(&buffer); + const int_info = int_ty.intInfo(target); + const backing_bits = self.backingIntBits(int_info.bits) orelse { + return self.todo("implement composite int switch", .{}); + }; + break :blk if (backing_bits <= 32) @as(u32, 1) else 2; + }, + else => return self.todo("implement switch for type {s}", .{@tagName(cond_ty.zigTypeTag())}), // TODO: Figure out which types apply here, and work around them as we can only do integers. }; - try self.func.body.emit(self.spv.gpa, .OpStore, .{ - .pointer = dst_ptr_id, - .object = src_val_id, - .memory_access = access, - }); + const num_cases = switch_br.data.cases_len; + + // Compute the total number of arms that we need. + // Zig switches are grouped by condition, so we need to loop through all of them + const num_conditions = blk: { + var extra_index: usize = switch_br.end; + var case_i: u32 = 0; + var num_conditions: u32 = 0; + while (case_i < num_cases) : (case_i += 1) { + const case = self.air.extraData(Air.SwitchBr.Case, extra_index); + const case_body = self.air.extra[case.end + case.data.items_len ..][0..case.data.body_len]; + extra_index = case.end + case.data.items_len + case_body.len; + num_conditions += case.data.items_len; + } + break :blk num_conditions; + }; + + // First, pre-allocate the labels for the cases. + const first_case_label = self.spv.allocIds(num_cases); + // We always need the default case - if zig has none, we will generate unreachable there. + const default = self.spv.allocId(); + + // Emit the instruction before generating the blocks. + try self.func.body.emitRaw(self.spv.gpa, .OpSwitch, 2 + (cond_words + 1) * num_conditions); + self.func.body.writeOperand(IdRef, cond); + self.func.body.writeOperand(IdRef, default); + + // Emit each of the cases + { + var extra_index: usize = switch_br.end; + var case_i: u32 = 0; + while (case_i < num_cases) : (case_i += 1) { + // SPIR-V needs a literal here, which' width depends on the case condition. + const case = self.air.extraData(Air.SwitchBr.Case, extra_index); + const items = @ptrCast([]const Air.Inst.Ref, self.air.extra[case.end..][0..case.data.items_len]); + const case_body = self.air.extra[case.end + items.len ..][0..case.data.body_len]; + extra_index = case.end + case.data.items_len + case_body.len; + + const label = IdRef{ .id = first_case_label.id + case_i }; + + for (items) |item| { + const value = self.air.value(item) orelse { + return self.todo("switch on runtime value???", .{}); + }; + const int_val = switch (cond_ty.zigTypeTag()) { + .Int => if (cond_ty.isSignedInt()) @bitCast(u64, value.toSignedInt(target)) else value.toUnsignedInt(target), + .Enum => blk: { + var int_buffer: Value.Payload.U64 = undefined; + // TODO: figure out of cond_ty is correct (something with enum literals) + break :blk value.enumToInt(cond_ty, &int_buffer).toUnsignedInt(target); // TODO: composite integer constants + }, + else => unreachable, + }; + const int_lit: spec.LiteralContextDependentNumber = switch (cond_words) { + 1 => .{ .uint32 = @intCast(u32, int_val) }, + 2 => .{ .uint64 = int_val }, + else => unreachable, + }; + self.func.body.writeOperand(spec.LiteralContextDependentNumber, int_lit); + self.func.body.writeOperand(IdRef, label); + } + } + } + + // Now, finally, we can start emitting each of the cases. + var extra_index: usize = switch_br.end; + var case_i: u32 = 0; + while (case_i < num_cases) : (case_i += 1) { + const case = self.air.extraData(Air.SwitchBr.Case, extra_index); + const items = @ptrCast([]const Air.Inst.Ref, self.air.extra[case.end..][0..case.data.items_len]); + const case_body = self.air.extra[case.end + items.len ..][0..case.data.body_len]; + extra_index = case.end + case.data.items_len + case_body.len; + + const label = IdResult{ .id = first_case_label.id + case_i }; + + try self.beginSpvBlock(label); + try self.genBody(case_body); + } + + const else_body = self.air.extra[extra_index..][0..switch_br.data.else_body_len]; + try self.beginSpvBlock(default); + if (else_body.len != 0) { + try self.genBody(else_body); + } else { + try self.func.body.emit(self.spv.gpa, .OpUnreachable, {}); + } } fn airUnreach(self: *DeclGen) !void { @@ -1158,4 +2692,47 @@ pub const DeclGen = struct { return null; } + + fn airCall(self: *DeclGen, inst: Air.Inst.Index, modifier: std.builtin.CallModifier) !?IdRef { + _ = modifier; + + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const extra = self.air.extraData(Air.Call, pl_op.payload); + const args = @ptrCast([]const Air.Inst.Ref, self.air.extra[extra.end..][0..extra.data.args_len]); + const callee_ty = self.air.typeOf(pl_op.operand); + const zig_fn_ty = switch (callee_ty.zigTypeTag()) { + .Fn => callee_ty, + .Pointer => return self.fail("cannot call function pointers", .{}), + else => unreachable, + }; + const fn_info = zig_fn_ty.fnInfo(); + const return_type = fn_info.return_type; + + const result_type_id = try self.resolveTypeId(return_type); + const result_id = self.spv.allocId(); + const callee_id = try self.resolve(pl_op.operand); + + try self.func.body.emitRaw(self.spv.gpa, .OpFunctionCall, 3 + args.len); + self.func.body.writeOperand(spec.IdResultType, result_type_id); + self.func.body.writeOperand(spec.IdResult, result_id); + self.func.body.writeOperand(spec.IdRef, callee_id); + + for (args) |arg| { + const arg_id = try self.resolve(arg); + const arg_ty = self.air.typeOf(arg); + if (!arg_ty.hasRuntimeBitsIgnoreComptime()) continue; + + self.func.body.writeOperand(spec.IdRef, arg_id); + } + + if (return_type.isNoReturn()) { + try self.func.body.emit(self.spv.gpa, .OpUnreachable, {}); + } + + if (self.liveness.isUnused(inst) or !return_type.hasRuntimeBitsIgnoreComptime()) { + return null; + } + + return result_id; + } }; diff --git a/src/codegen/spirv/Assembler.zig b/src/codegen/spirv/Assembler.zig index 6e77818fa5..eebf43866d 100644 --- a/src/codegen/spirv/Assembler.zig +++ b/src/codegen/spirv/Assembler.zig @@ -135,7 +135,7 @@ const AsmValue = union(enum) { return switch (self) { .just_declared, .unresolved_forward_reference => unreachable, .value => |result| result, - .ty => |ref| spv.typeResultId(ref).toRef(), + .ty => |ref| spv.typeId(ref), }; } }; @@ -239,12 +239,17 @@ fn todo(self: *Assembler, comptime fmt: []const u8, args: anytype) Error { /// If this function returns `error.AssembleFail`, an explanatory /// error message has already been emitted into `self.errors`. fn processInstruction(self: *Assembler) !void { - const result = switch (self.inst.opcode.class()) { - .TypeDeclaration => try self.processTypeInstruction(), - else => if (try self.processGenericInstruction()) |result| - result - else - return, + const result = switch (self.inst.opcode) { + .OpEntryPoint => { + return self.fail(0, "cannot export entry points via OpEntryPoint, export the kernel using callconv(.Kernel)", .{}); + }, + else => switch (self.inst.opcode.class()) { + .TypeDeclaration => try self.processTypeInstruction(), + else => if (try self.processGenericInstruction()) |result| + result + else + return, + }, }; const result_ref = self.inst.result().?; @@ -266,27 +271,28 @@ fn processTypeInstruction(self: *Assembler) !AsmValue { .OpTypeVoid => SpvType.initTag(.void), .OpTypeBool => SpvType.initTag(.bool), .OpTypeInt => blk: { - const payload = try self.spv.arena.create(SpvType.Payload.Int); const signedness: std.builtin.Signedness = switch (operands[2].literal32) { 0 => .unsigned, 1 => .signed, else => { // TODO: Improve source location. - return self.fail(0, "'{}' is not a valid signedness (expected 0 or 1)", .{operands[2].literal32}); + return self.fail(0, "{} is not a valid signedness (expected 0 or 1)", .{operands[2].literal32}); }, }; - payload.* = .{ - .width = operands[1].literal32, - .signedness = signedness, + const width = std.math.cast(u16, operands[1].literal32) orelse { + return self.fail(0, "int type of {} bits is too large", .{operands[1].literal32}); }; - break :blk SpvType.initPayload(&payload.base); + break :blk try SpvType.int(self.spv.arena, signedness, width); }, .OpTypeFloat => blk: { - const payload = try self.spv.arena.create(SpvType.Payload.Float); - payload.* = .{ - .width = operands[1].literal32, - }; - break :blk SpvType.initPayload(&payload.base); + const bits = operands[1].literal32; + switch (bits) { + 16, 32, 64 => {}, + else => { + return self.fail(0, "{} is not a valid bit count for floats (expected 16, 32 or 64)", .{bits}); + }, + } + break :blk SpvType.float(@intCast(u16, bits)); }, .OpTypeVector => blk: { const payload = try self.spv.arena.create(SpvType.Payload.Vector); @@ -382,10 +388,7 @@ fn processTypeInstruction(self: *Assembler) !AsmValue { payload.* = .{ .storage_class = @intToEnum(spec.StorageClass, operands[1].value), .child_type = try self.resolveTypeRef(operands[2].ref_id), - // TODO: Fetch these values from decorations. - .array_stride = 0, - .alignment = null, - .max_byte_offset = null, + // TODO: Fetch decorations }; break :blk SpvType.initPayload(&payload.base); }, @@ -434,11 +437,16 @@ fn processGenericInstruction(self: *Assembler) !?AsmValue { .Annotation => &self.spv.sections.annotations, .TypeDeclaration => unreachable, // Handled elsewhere. else => switch (self.inst.opcode) { - .OpEntryPoint => &self.spv.sections.entry_points, + .OpEntryPoint => unreachable, .OpExecutionMode, .OpExecutionModeId => &self.spv.sections.execution_modes, .OpVariable => switch (@intToEnum(spec.StorageClass, operands[2].value)) { .Function => &self.func.prologue, - else => &self.spv.sections.types_globals_constants, + else => { + // This is currently disabled because global variables are required to be + // emitted in the proper order, and this should be honored in inline assembly + // as well. + return self.todo("global variables", .{}); + }, }, // Default case - to be worked out further. else => &self.func.body, @@ -485,7 +493,7 @@ fn processGenericInstruction(self: *Assembler) !?AsmValue { section.instructions.items[first_word] |= @as(u32, @intCast(u16, actual_word_count)) << 16 | @enumToInt(self.inst.opcode); if (maybe_result_id) |result| { - return AsmValue{ .value = result.toRef() }; + return AsmValue{ .value = result }; } return null; } @@ -753,22 +761,19 @@ fn parseContextDependentNumber(self: *Assembler) !void { const tok = self.currentToken(); const result_type_ref = try self.resolveTypeRef(self.inst.operands.items[0].ref_id); - const result_type = self.spv.type_cache.keys()[result_type_ref]; - switch (result_type.tag()) { - .int => { - const int = result_type.castTag(.int).?; - try self.parseContextDependentInt(int.signedness, int.width); - }, - .float => { - const width = result_type.castTag(.float).?.width; - switch (width) { - 16 => try self.parseContextDependentFloat(16), - 32 => try self.parseContextDependentFloat(32), - 64 => try self.parseContextDependentFloat(64), - else => return self.fail(tok.start, "cannot parse {}-bit float literal", .{width}), - } - }, - else => return self.fail(tok.start, "cannot parse literal constant {s}", .{@tagName(result_type.tag())}), + const result_type = self.spv.type_cache.keys()[@enumToInt(result_type_ref)]; + if (result_type.isInt()) { + try self.parseContextDependentInt(result_type.intSignedness(), result_type.intFloatBits()); + } else if (result_type.isFloat()) { + const width = result_type.intFloatBits(); + switch (width) { + 16 => try self.parseContextDependentFloat(16), + 32 => try self.parseContextDependentFloat(32), + 64 => try self.parseContextDependentFloat(64), + else => return self.fail(tok.start, "cannot parse {}-bit float literal", .{width}), + } + } else { + return self.fail(tok.start, "cannot parse literal constant {s}", .{@tagName(result_type.tag())}); } } diff --git a/src/codegen/spirv/Module.zig b/src/codegen/spirv/Module.zig index 3562e87be4..7ae6cb0c6a 100644 --- a/src/codegen/spirv/Module.zig +++ b/src/codegen/spirv/Module.zig @@ -39,22 +39,68 @@ pub const Fn = struct { /// This section should also contain the OpFunctionEnd instruction marking /// the end of this function definition. body: Section = .{}, + /// The decl dependencies that this function depends on. + decl_deps: std.ArrayListUnmanaged(Decl.Index) = .{}, /// Reset this function without deallocating resources, so that /// it may be used to emit code for another function. pub fn reset(self: *Fn) void { self.prologue.reset(); self.body.reset(); + self.decl_deps.items.len = 0; } /// Free the resources owned by this function. pub fn deinit(self: *Fn, a: Allocator) void { self.prologue.deinit(a); self.body.deinit(a); + self.decl_deps.deinit(a); self.* = undefined; } }; +/// Declarations, both functions and globals, can have dependencies. These are used for 2 things: +/// - Globals must be declared before they are used, also between globals. The compiler processes +/// globals unordered, so we must use the dependencies here to figure out how to order the globals +/// in the final module. The Globals structure is also used for that. +/// - Entry points must declare the complete list of OpVariable instructions that they access. +/// For these we use the same dependency structure. +/// In this mechanism, globals will only depend on other globals, while functions may depend on +/// globals or other functions. +pub const Decl = struct { + /// Index to refer to a Decl by. + pub const Index = enum(u32) { _ }; + + /// The result-id to be used for this declaration. This is the final result-id + /// of the decl, which may be an OpFunction, OpVariable, or the result of a sequence + /// of OpSpecConstantOp operations. + result_id: IdRef, + /// The offset of the first dependency of this decl in the `decl_deps` array. + begin_dep: u32, + /// The past-end offset of the dependencies of this decl in the `decl_deps` array. + end_dep: u32, +}; + +/// Globals must be kept in order: operations involving globals must be ordered +/// so that the global declaration precedes any usage. +pub const Global = struct { + /// This is the result-id of the OpVariable instruction that declares the global. + result_id: IdRef, + /// The offset into `self.globals.section` of the first instruction of this global + /// declaration. + begin_inst: u32, + /// The past-end offset into `self.flobals.section`. + end_inst: u32, +}; + +/// This models a kernel entry point. +pub const EntryPoint = struct { + /// The declaration that should be exported. + decl_index: Decl.Index, + /// The name of the kernel to be exported. + name: []const u8, +}; + /// A general-purpose allocator which may be used to allocate resources for this module gpa: Allocator, @@ -69,13 +115,13 @@ sections: struct { extensions: Section = .{}, // OpExtInstImport instructions - skip for now. // memory model defined by target, not required here. - /// OpEntryPoint instructions. - entry_points: Section = .{}, + /// OpEntryPoint instructions - Handled by `self.entry_points`. /// OpExecutionMode and OpExecutionModeId instructions. execution_modes: Section = .{}, /// OpString, OpSourcExtension, OpSource, OpSourceContinued. debug_strings: Section = .{}, - // OpName, OpMemberName - skip for now. + // OpName, OpMemberName. + debug_names: Section = .{}, // OpModuleProcessed - skip for now. /// Annotation instructions (OpDecorate etc). annotations: Section = .{}, @@ -101,6 +147,26 @@ source_file_names: std.StringHashMapUnmanaged(IdRef) = .{}, /// Note: Uses ArrayHashMap which is insertion ordered, so that we may refer to other types by index (Type.Ref). type_cache: TypeCache = .{}, +/// Set of Decls, referred to by Decl.Index. +decls: std.ArrayListUnmanaged(Decl) = .{}, + +/// List of dependencies, per decl. This list holds all the dependencies, sliced by the +/// begin_dep and end_dep in `self.decls`. +decl_deps: std.ArrayListUnmanaged(Decl.Index) = .{}, + +/// The list of entry points that should be exported from this module. +entry_points: std.ArrayListUnmanaged(EntryPoint) = .{}, + +/// The fields in this structure help to maintain the required order for global variables. +globals: struct { + /// Set of globals, referred to by Decl.Index. + globals: std.AutoArrayHashMapUnmanaged(Decl.Index, Global) = .{}, + /// This pseudo-section contains the initialization code for all the globals. Instructions from + /// here are reordered when flushing the module. Its contents should be part of the + /// `types_globals_constants` SPIR-V section. + section: Section = .{}, +} = .{}, + pub fn init(gpa: Allocator, arena: Allocator) Module { return .{ .gpa = gpa, @@ -112,9 +178,9 @@ pub fn init(gpa: Allocator, arena: Allocator) Module { pub fn deinit(self: *Module) void { self.sections.capabilities.deinit(self.gpa); self.sections.extensions.deinit(self.gpa); - self.sections.entry_points.deinit(self.gpa); self.sections.execution_modes.deinit(self.gpa); self.sections.debug_strings.deinit(self.gpa); + self.sections.debug_names.deinit(self.gpa); self.sections.annotations.deinit(self.gpa); self.sections.types_globals_constants.deinit(self.gpa); self.sections.functions.deinit(self.gpa); @@ -122,6 +188,14 @@ pub fn deinit(self: *Module) void { self.source_file_names.deinit(self.gpa); self.type_cache.deinit(self.gpa); + self.decls.deinit(self.gpa); + self.decl_deps.deinit(self.gpa); + + self.entry_points.deinit(self.gpa); + + self.globals.globals.deinit(self.gpa); + self.globals.section.deinit(self.gpa); + self.* = undefined; } @@ -130,32 +204,138 @@ pub fn allocId(self: *Module) spec.IdResult { return .{ .id = self.next_result_id }; } +pub fn allocIds(self: *Module, n: u32) spec.IdResult { + defer self.next_result_id += n; + return .{ .id = self.next_result_id }; +} + pub fn idBound(self: Module) Word { return self.next_result_id; } +fn orderGlobalsInto( + self: *Module, + decl_index: Decl.Index, + section: *Section, + seen: *std.DynamicBitSetUnmanaged, +) !void { + const decl = self.declPtr(decl_index); + const deps = self.decl_deps.items[decl.begin_dep..decl.end_dep]; + const global = self.globalPtr(decl_index).?; + const insts = self.globals.section.instructions.items[global.begin_inst..global.end_inst]; + + seen.set(@enumToInt(decl_index)); + + for (deps) |dep| { + if (!seen.isSet(@enumToInt(dep))) { + try self.orderGlobalsInto(dep, section, seen); + } + } + + try section.instructions.appendSlice(self.gpa, insts); +} + +fn orderGlobals(self: *Module) !Section { + const globals = self.globals.globals.keys(); + + var seen = try std.DynamicBitSetUnmanaged.initEmpty(self.gpa, self.decls.items.len); + defer seen.deinit(self.gpa); + + var ordered_globals = Section{}; + errdefer ordered_globals.deinit(self.gpa); + + for (globals) |decl_index| { + if (!seen.isSet(@enumToInt(decl_index))) { + try self.orderGlobalsInto(decl_index, &ordered_globals, &seen); + } + } + + return ordered_globals; +} + +fn addEntryPointDeps( + self: *Module, + decl_index: Decl.Index, + seen: *std.DynamicBitSetUnmanaged, + interface: *std.ArrayList(IdRef), +) !void { + const decl = self.declPtr(decl_index); + const deps = self.decl_deps.items[decl.begin_dep..decl.end_dep]; + + seen.set(@enumToInt(decl_index)); + + if (self.globalPtr(decl_index)) |global| { + try interface.append(global.result_id); + } + + for (deps) |dep| { + if (!seen.isSet(@enumToInt(dep))) { + try self.addEntryPointDeps(dep, seen, interface); + } + } +} + +fn entryPoints(self: *Module) !Section { + var entry_points = Section{}; + errdefer entry_points.deinit(self.gpa); + + var interface = std.ArrayList(IdRef).init(self.gpa); + defer interface.deinit(); + + var seen = try std.DynamicBitSetUnmanaged.initEmpty(self.gpa, self.decls.items.len); + defer seen.deinit(self.gpa); + + for (self.entry_points.items) |entry_point| { + interface.items.len = 0; + seen.setRangeValue(.{ .start = 0, .end = self.decls.items.len }, false); + + try self.addEntryPointDeps(entry_point.decl_index, &seen, &interface); + + const entry_point_id = self.declPtr(entry_point.decl_index).result_id; + try entry_points.emit(self.gpa, .OpEntryPoint, .{ + .execution_model = .Kernel, + .entry_point = entry_point_id, + .name = entry_point.name, + .interface = interface.items, + }); + } + + return entry_points; +} + /// Emit this module as a spir-v binary. -pub fn flush(self: Module, file: std.fs.File) !void { +pub fn flush(self: *Module, file: std.fs.File) !void { // See SPIR-V Spec section 2.3, "Physical Layout of a SPIR-V Module and Instruction" const header = [_]Word{ spec.magic_number, - (1 << 16) | (5 << 8), + // TODO: From cpu features + // Emit SPIR-V 1.4 for now. This is the highest version that Intel's CPU OpenCL supports. + (1 << 16) | (4 << 8), 0, // TODO: Register Zig compiler magic number. self.idBound(), 0, // Schema (currently reserved for future use) }; + // TODO: Perform topological sort on the globals. + var globals = try self.orderGlobals(); + defer globals.deinit(self.gpa); + + var entry_points = try self.entryPoints(); + defer entry_points.deinit(self.gpa); + // Note: needs to be kept in order according to section 2.3! const buffers = &[_][]const Word{ &header, self.sections.capabilities.toWords(), self.sections.extensions.toWords(), - self.sections.entry_points.toWords(), + entry_points.toWords(), self.sections.execution_modes.toWords(), self.sections.debug_strings.toWords(), + self.sections.debug_names.toWords(), self.sections.annotations.toWords(), self.sections.types_globals_constants.toWords(), + globals.toWords(), self.sections.functions.toWords(), }; @@ -175,9 +355,10 @@ pub fn flush(self: Module, file: std.fs.File) !void { } /// Merge the sections making up a function declaration into this module. -pub fn addFunction(self: *Module, func: Fn) !void { +pub fn addFunction(self: *Module, decl_index: Decl.Index, func: Fn) !void { try self.sections.functions.append(self.gpa, func.prologue); try self.sections.functions.append(self.gpa, func.body); + try self.declareDeclDeps(decl_index, func.decl_deps.items); } /// Fetch the result-id of an OpString instruction that encodes the path of the source @@ -188,7 +369,7 @@ pub fn resolveSourceFileName(self: *Module, decl: *ZigDecl) !IdRef { const result = try self.source_file_names.getOrPut(self.gpa, path); if (!result.found_existing) { const file_result_id = self.allocId(); - result.value_ptr.* = file_result_id.toRef(); + result.value_ptr.* = file_result_id; try self.sections.debug_strings.emit(self.gpa, .OpString, .{ .id_result = file_result_id, .string = path, @@ -197,7 +378,7 @@ pub fn resolveSourceFileName(self: *Module, decl: *ZigDecl) !IdRef { try self.sections.debug_strings.emit(self.gpa, .OpSource, .{ .source_language = .Unknown, // TODO: Register Zig source language. .version = 0, // TODO: Zig version as u32? - .file = file_result_id.toRef(), + .file = file_result_id, .source = null, // TODO: Store actual source also? }); } @@ -216,22 +397,21 @@ pub fn resolveType(self: *Module, ty: Type) !Type.Ref { result.value_ptr.* = try self.emitType(ty); } - return result.index; + return @intToEnum(Type.Ref, result.index); } -pub fn resolveTypeId(self: *Module, ty: Type) !IdRef { - const type_ref = try self.resolveType(ty); - return self.typeResultId(type_ref); +pub fn resolveTypeId(self: *Module, ty: Type) !IdResultType { + const ty_ref = try self.resolveType(ty); + return self.typeId(ty_ref); +} + +pub fn typeRefType(self: Module, ty_ref: Type.Ref) Type { + return self.type_cache.keys()[@enumToInt(ty_ref)]; } /// Get the result-id of a particular type, by reference. Asserts type_ref is valid. -pub fn typeResultId(self: Module, type_ref: Type.Ref) IdResultType { - return self.type_cache.values()[type_ref]; -} - -/// Get the result-id of a particular type as IdRef, by Type.Ref. Asserts type_ref is valid. -pub fn typeRefId(self: Module, type_ref: Type.Ref) IdRef { - return self.type_cache.values()[type_ref].toRef(); +pub fn typeId(self: Module, ty_ref: Type.Ref) IdResultType { + return self.type_cache.values()[@enumToInt(ty_ref)]; } /// Unconditionally emit a spir-v type into the appropriate section. @@ -240,47 +420,94 @@ pub fn typeRefId(self: Module, type_ref: Type.Ref) IdRef { /// Note: This function does not attempt to perform any validation on the type. /// The type is emitted in a shallow fashion; any child types should already /// be emitted at this point. -pub fn emitType(self: *Module, ty: Type) !IdResultType { +pub fn emitType(self: *Module, ty: Type) error{OutOfMemory}!IdResultType { const result_id = self.allocId(); - const ref_id = result_id.toRef(); + const ref_id = result_id; const types = &self.sections.types_globals_constants; - const annotations = &self.sections.annotations; + const debug_names = &self.sections.debug_names; const result_id_operand = .{ .id_result = result_id }; switch (ty.tag()) { - .void => try types.emit(self.gpa, .OpTypeVoid, result_id_operand), - .bool => try types.emit(self.gpa, .OpTypeBool, result_id_operand), - .int => { - const signedness: spec.LiteralInteger = switch (ty.payload(.int).signedness) { + .void => { + try types.emit(self.gpa, .OpTypeVoid, result_id_operand); + try debug_names.emit(self.gpa, .OpName, .{ + .target = result_id, + .name = "void", + }); + }, + .bool => { + try types.emit(self.gpa, .OpTypeBool, result_id_operand); + try debug_names.emit(self.gpa, .OpName, .{ + .target = result_id, + .name = "bool", + }); + }, + .u8, + .u16, + .u32, + .u64, + .i8, + .i16, + .i32, + .i64, + .int, + => { + // TODO: Kernels do not support OpTypeInt that is signed. We can probably + // can get rid of the signedness all together, in Shaders also. + const bits = ty.intFloatBits(); + const signedness: spec.LiteralInteger = switch (ty.intSignedness()) { .unsigned => 0, .signed => 1, }; try types.emit(self.gpa, .OpTypeInt, .{ .id_result = result_id, - .width = ty.payload(.int).width, + .width = bits, .signedness = signedness, }); + + const ui: []const u8 = switch (signedness) { + 0 => "u", + 1 => "i", + else => unreachable, + }; + const name = try std.fmt.allocPrint(self.gpa, "{s}{}", .{ ui, bits }); + defer self.gpa.free(name); + + try debug_names.emit(self.gpa, .OpName, .{ + .target = result_id, + .name = name, + }); + }, + .f16, .f32, .f64 => { + const bits = ty.intFloatBits(); + try types.emit(self.gpa, .OpTypeFloat, .{ + .id_result = result_id, + .width = bits, + }); + + const name = try std.fmt.allocPrint(self.gpa, "f{}", .{bits}); + defer self.gpa.free(name); + try debug_names.emit(self.gpa, .OpName, .{ + .target = result_id, + .name = name, + }); }, - .float => try types.emit(self.gpa, .OpTypeFloat, .{ - .id_result = result_id, - .width = ty.payload(.float).width, - }), .vector => try types.emit(self.gpa, .OpTypeVector, .{ .id_result = result_id, - .component_type = self.typeResultId(ty.childType()).toRef(), + .component_type = self.typeId(ty.childType()), .component_count = ty.payload(.vector).component_count, }), .matrix => try types.emit(self.gpa, .OpTypeMatrix, .{ .id_result = result_id, - .column_type = self.typeResultId(ty.childType()).toRef(), + .column_type = self.typeId(ty.childType()), .column_count = ty.payload(.matrix).column_count, }), .image => { const info = ty.payload(.image); try types.emit(self.gpa, .OpTypeImage, .{ .id_result = result_id, - .sampled_type = self.typeResultId(ty.childType()).toRef(), + .sampled_type = self.typeId(ty.childType()), .dim = info.dim, .depth = @enumToInt(info.depth), .arrayed = @boolToInt(info.arrayed), @@ -293,28 +520,34 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType { .sampler => try types.emit(self.gpa, .OpTypeSampler, result_id_operand), .sampled_image => try types.emit(self.gpa, .OpTypeSampledImage, .{ .id_result = result_id, - .image_type = self.typeResultId(ty.childType()).toRef(), + .image_type = self.typeId(ty.childType()), }), .array => { const info = ty.payload(.array); assert(info.length != 0); + + const size_type = Type.initTag(.u32); + const size_type_id = try self.resolveTypeId(size_type); + const length_id = self.allocId(); + try self.emitConstant(size_type_id, length_id, .{ .uint32 = info.length }); + try types.emit(self.gpa, .OpTypeArray, .{ .id_result = result_id, - .element_type = self.typeResultId(ty.childType()).toRef(), - .length = .{ .id = 0 }, // TODO: info.length must be emitted as constant! + .element_type = self.typeId(ty.childType()), + .length = length_id, }); if (info.array_stride != 0) { - try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); + try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); } }, .runtime_array => { const info = ty.payload(.runtime_array); try types.emit(self.gpa, .OpTypeRuntimeArray, .{ .id_result = result_id, - .element_type = self.typeResultId(ty.childType()).toRef(), + .element_type = self.typeId(ty.childType()), }); if (info.array_stride != 0) { - try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); + try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); } }, .@"struct" => { @@ -322,7 +555,7 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType { try types.emitRaw(self.gpa, .OpTypeStruct, 1 + info.members.len); types.writeOperand(IdResult, result_id); for (info.members) |member| { - types.writeOperand(IdRef, self.typeResultId(member.ty).toRef()); + types.writeOperand(IdRef, self.typeId(member.ty)); } try self.decorateStruct(ref_id, info); }, @@ -335,25 +568,25 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType { try types.emit(self.gpa, .OpTypePointer, .{ .id_result = result_id, .storage_class = info.storage_class, - .type = self.typeResultId(ty.childType()).toRef(), + .type = self.typeId(ty.childType()), }); if (info.array_stride != 0) { - try annotations.decorate(self.gpa, ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); + try self.decorate(ref_id, .{ .ArrayStride = .{ .array_stride = info.array_stride } }); } - if (info.alignment) |alignment| { - try annotations.decorate(self.gpa, ref_id, .{ .Alignment = .{ .alignment = alignment } }); + if (info.alignment != 0) { + try self.decorate(ref_id, .{ .Alignment = .{ .alignment = info.alignment } }); } if (info.max_byte_offset) |max_byte_offset| { - try annotations.decorate(self.gpa, ref_id, .{ .MaxByteOffset = .{ .max_byte_offset = max_byte_offset } }); + try self.decorate(ref_id, .{ .MaxByteOffset = .{ .max_byte_offset = max_byte_offset } }); } }, .function => { const info = ty.payload(.function); try types.emitRaw(self.gpa, .OpTypeFunction, 2 + info.parameters.len); types.writeOperand(IdResult, result_id); - types.writeOperand(IdRef, self.typeResultId(info.return_type).toRef()); + types.writeOperand(IdRef, self.typeId(info.return_type)); for (info.parameters) |parameter_type| { - types.writeOperand(IdRef, self.typeResultId(parameter_type).toRef()); + types.writeOperand(IdRef, self.typeId(parameter_type)); } }, .event => try types.emit(self.gpa, .OpTypeEvent, result_id_operand), @@ -368,23 +601,30 @@ pub fn emitType(self: *Module, ty: Type) !IdResultType { .named_barrier => try types.emit(self.gpa, .OpTypeNamedBarrier, result_id_operand), } - return result_id.toResultType(); + return result_id; } fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct) !void { - const annotations = &self.sections.annotations; + const debug_names = &self.sections.debug_names; + + if (info.name.len != 0) { + try debug_names.emit(self.gpa, .OpName, .{ + .target = target, + .name = info.name, + }); + } // Decorations for the struct type itself. if (info.decorations.block) - try annotations.decorate(self.gpa, target, .Block); + try self.decorate(target, .Block); if (info.decorations.buffer_block) - try annotations.decorate(self.gpa, target, .BufferBlock); + try self.decorate(target, .BufferBlock); if (info.decorations.glsl_shared) - try annotations.decorate(self.gpa, target, .GLSLShared); + try self.decorate(target, .GLSLShared); if (info.decorations.glsl_packed) - try annotations.decorate(self.gpa, target, .GLSLPacked); + try self.decorate(target, .GLSLPacked); if (info.decorations.c_packed) - try annotations.decorate(self.gpa, target, .CPacked); + try self.decorate(target, .CPacked); // Decorations for the struct members. const extra = info.member_decoration_extra; @@ -392,71 +632,89 @@ fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct for (info.members, 0..) |member, i| { const d = member.decorations; const index = @intCast(Word, i); + + if (member.name.len != 0) { + try debug_names.emit(self.gpa, .OpMemberName, .{ + .type = target, + .member = index, + .name = member.name, + }); + } + + switch (member.offset) { + .none => {}, + else => try self.decorateMember( + target, + index, + .{ .Offset = .{ .byte_offset = @enumToInt(member.offset) } }, + ), + } + switch (d.matrix_layout) { - .row_major => try annotations.decorateMember(self.gpa, target, index, .RowMajor), - .col_major => try annotations.decorateMember(self.gpa, target, index, .ColMajor), + .row_major => try self.decorateMember(target, index, .RowMajor), + .col_major => try self.decorateMember(target, index, .ColMajor), .none => {}, } if (d.matrix_layout != .none) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .MatrixStride = .{ .matrix_stride = extra[extra_i] }, }); extra_i += 1; } if (d.no_perspective) - try annotations.decorateMember(self.gpa, target, index, .NoPerspective); + try self.decorateMember(target, index, .NoPerspective); if (d.flat) - try annotations.decorateMember(self.gpa, target, index, .Flat); + try self.decorateMember(target, index, .Flat); if (d.patch) - try annotations.decorateMember(self.gpa, target, index, .Patch); + try self.decorateMember(target, index, .Patch); if (d.centroid) - try annotations.decorateMember(self.gpa, target, index, .Centroid); + try self.decorateMember(target, index, .Centroid); if (d.sample) - try annotations.decorateMember(self.gpa, target, index, .Sample); + try self.decorateMember(target, index, .Sample); if (d.invariant) - try annotations.decorateMember(self.gpa, target, index, .Invariant); + try self.decorateMember(target, index, .Invariant); if (d.@"volatile") - try annotations.decorateMember(self.gpa, target, index, .Volatile); + try self.decorateMember(target, index, .Volatile); if (d.coherent) - try annotations.decorateMember(self.gpa, target, index, .Coherent); + try self.decorateMember(target, index, .Coherent); if (d.non_writable) - try annotations.decorateMember(self.gpa, target, index, .NonWritable); + try self.decorateMember(target, index, .NonWritable); if (d.non_readable) - try annotations.decorateMember(self.gpa, target, index, .NonReadable); + try self.decorateMember(target, index, .NonReadable); if (d.builtin) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .BuiltIn = .{ .built_in = @intToEnum(spec.BuiltIn, extra[extra_i]) }, }); extra_i += 1; } if (d.stream) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .Stream = .{ .stream_number = extra[extra_i] }, }); extra_i += 1; } if (d.location) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .Location = .{ .location = extra[extra_i] }, }); extra_i += 1; } if (d.component) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .Component = .{ .component = extra[extra_i] }, }); extra_i += 1; } if (d.xfb_buffer) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .XfbBuffer = .{ .xfb_buffer_number = extra[extra_i] }, }); extra_i += 1; } if (d.xfb_stride) { - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .XfbStride = .{ .xfb_stride = extra[extra_i] }, }); extra_i += 1; @@ -465,10 +723,150 @@ fn decorateStruct(self: *Module, target: IdRef, info: *const Type.Payload.Struct const len = extra[extra_i]; extra_i += 1; const semantic = @ptrCast([*]const u8, &extra[extra_i])[0..len]; - try annotations.decorateMember(self.gpa, target, index, .{ + try self.decorateMember(target, index, .{ .UserSemantic = .{ .semantic = semantic }, }); extra_i += std.math.divCeil(u32, extra_i, @sizeOf(u32)) catch unreachable; } } } + +pub fn simpleStructType(self: *Module, members: []const Type.Payload.Struct.Member) !Type.Ref { + const payload = try self.arena.create(Type.Payload.Struct); + payload.* = .{ + .members = try self.arena.dupe(Type.Payload.Struct.Member, members), + .decorations = .{}, + }; + return try self.resolveType(Type.initPayload(&payload.base)); +} + +pub fn arrayType(self: *Module, len: u32, ty: Type.Ref) !Type.Ref { + const payload = try self.arena.create(Type.Payload.Array); + payload.* = .{ + .element_type = ty, + .length = len, + }; + return try self.resolveType(Type.initPayload(&payload.base)); +} + +pub fn ptrType( + self: *Module, + child: Type.Ref, + storage_class: spec.StorageClass, + alignment: u32, +) !Type.Ref { + const ptr_payload = try self.arena.create(Type.Payload.Pointer); + ptr_payload.* = .{ + .storage_class = storage_class, + .child_type = child, + .alignment = alignment, + }; + return try self.resolveType(Type.initPayload(&ptr_payload.base)); +} + +pub fn changePtrStorageClass(self: *Module, ptr_ty_ref: Type.Ref, new_storage_class: spec.StorageClass) !Type.Ref { + const payload = try self.arena.create(Type.Payload.Pointer); + payload.* = self.typeRefType(ptr_ty_ref).payload(.pointer).*; + payload.storage_class = new_storage_class; + return try self.resolveType(Type.initPayload(&payload.base)); +} + +pub fn emitConstant( + self: *Module, + ty_id: IdRef, + result_id: IdRef, + value: spec.LiteralContextDependentNumber, +) !void { + try self.sections.types_globals_constants.emit(self.gpa, .OpConstant, .{ + .id_result_type = ty_id, + .id_result = result_id, + .value = value, + }); +} + +/// Decorate a result-id. +pub fn decorate( + self: *Module, + target: IdRef, + decoration: spec.Decoration.Extended, +) !void { + try self.sections.annotations.emit(self.gpa, .OpDecorate, .{ + .target = target, + .decoration = decoration, + }); +} + +/// Decorate a result-id which is a member of some struct. +pub fn decorateMember( + self: *Module, + structure_type: IdRef, + member: u32, + decoration: spec.Decoration.Extended, +) !void { + try self.sections.annotations.emit(self.gpa, .OpMemberDecorate, .{ + .structure_type = structure_type, + .member = member, + .decoration = decoration, + }); +} + +pub const DeclKind = enum { + func, + global, +}; + +pub fn allocDecl(self: *Module, kind: DeclKind) !Decl.Index { + try self.decls.append(self.gpa, .{ + .result_id = self.allocId(), + .begin_dep = undefined, + .end_dep = undefined, + }); + const index = @intToEnum(Decl.Index, @intCast(u32, self.decls.items.len - 1)); + switch (kind) { + .func => {}, + // If the decl represents a global, also allocate a global node. + .global => try self.globals.globals.putNoClobber(self.gpa, index, .{ + .result_id = undefined, + .begin_inst = undefined, + .end_inst = undefined, + }), + } + + return index; +} + +pub fn declPtr(self: *Module, index: Decl.Index) *Decl { + return &self.decls.items[@enumToInt(index)]; +} + +pub fn globalPtr(self: *Module, index: Decl.Index) ?*Global { + return self.globals.globals.getPtr(index); +} + +/// Declare ALL dependencies for a decl. +pub fn declareDeclDeps(self: *Module, decl_index: Decl.Index, deps: []const Decl.Index) !void { + const begin_dep = @intCast(u32, self.decl_deps.items.len); + try self.decl_deps.appendSlice(self.gpa, deps); + const end_dep = @intCast(u32, self.decl_deps.items.len); + + const decl = self.declPtr(decl_index); + decl.begin_dep = begin_dep; + decl.end_dep = end_dep; +} + +pub fn beginGlobal(self: *Module) u32 { + return @intCast(u32, self.globals.section.instructions.items.len); +} + +pub fn endGlobal(self: *Module, global_index: Decl.Index, begin_inst: u32) void { + const global = self.globalPtr(global_index).?; + global.begin_inst = begin_inst; + global.end_inst = @intCast(u32, self.globals.section.instructions.items.len); +} + +pub fn declareEntryPoint(self: *Module, decl_index: Decl.Index, name: []const u8) !void { + try self.entry_points.append(self.gpa, .{ + .decl_index = decl_index, + .name = try self.arena.dupe(u8, name), + }); +} diff --git a/src/codegen/spirv/Section.zig b/src/codegen/spirv/Section.zig index a76314f5fa..b6087bbc3b 100644 --- a/src/codegen/spirv/Section.zig +++ b/src/codegen/spirv/Section.zig @@ -65,32 +65,23 @@ pub fn emit( section.writeOperands(opcode.Operands(), operands); } -/// Decorate a result-id. -pub fn decorate( +pub fn emitSpecConstantOp( section: *Section, allocator: Allocator, - target: spec.IdRef, - decoration: spec.Decoration.Extended, + comptime opcode: spec.Opcode, + operands: opcode.Operands(), ) !void { - try section.emit(allocator, .OpDecorate, .{ - .target = target, - .decoration = decoration, - }); -} + const word_count = operandsSize(opcode.Operands(), operands); + try section.emitRaw(allocator, .OpSpecConstantOp, 1 + word_count); + section.writeOperand(spec.IdRef, operands.id_result_type); + section.writeOperand(spec.IdRef, operands.id_result); + section.writeOperand(Opcode, opcode); -/// Decorate a result-id which is a member of some struct. -pub fn decorateMember( - section: *Section, - allocator: Allocator, - structure_type: spec.IdRef, - member: u32, - decoration: spec.Decoration.Extended, -) !void { - try section.emit(allocator, .OpMemberDecorate, .{ - .structure_type = structure_type, - .member = member, - .decoration = decoration, - }); + const fields = @typeInfo(opcode.Operands()).Struct.fields; + // First 2 fields are always id_result_type and id_result. + inline for (fields[2..]) |field| { + section.writeOperand(field.type, @field(operands, field.name)); + } } pub fn writeWord(section: *Section, word: Word) void { @@ -122,7 +113,7 @@ fn writeOperands(section: *Section, comptime Operands: type, operands: Operands) pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void { switch (Operand) { - spec.IdResultType, spec.IdResult, spec.IdRef => section.writeWord(operand.id), + spec.IdResult => section.writeWord(operand.id), spec.LiteralInteger => section.writeWord(operand), @@ -258,9 +249,7 @@ fn operandsSize(comptime Operands: type, operands: Operands) usize { fn operandSize(comptime Operand: type, operand: Operand) usize { return switch (Operand) { - spec.IdResultType, spec.IdResult, - spec.IdRef, spec.LiteralInteger, spec.LiteralExtInstInteger, => 1, @@ -382,7 +371,9 @@ test "SPIR-V Section emit() - string" { }, section.instructions.items); } -test "SPIR-V Section emit()- extended mask" { +test "SPIR-V Section emit() - extended mask" { + if (@import("builtin").zig_backend == .stage1) return error.SkipZigTest; + var section = Section{}; defer section.deinit(std.testing.allocator); diff --git a/src/codegen/spirv/spec.zig b/src/codegen/spirv/spec.zig index 3978231829..60d16461cb 100644 --- a/src/codegen/spirv/spec.zig +++ b/src/codegen/spirv/spec.zig @@ -3,22 +3,11 @@ const Version = @import("std").builtin.Version; pub const Word = u32; -pub const IdResultType = struct { - id: Word, - pub fn toRef(self: IdResultType) IdRef { - return .{ .id = self.id }; - } -}; pub const IdResult = struct { id: Word, - pub fn toRef(self: IdResult) IdRef { - return .{ .id = self.id }; - } - pub fn toResultType(self: IdResult) IdResultType { - return .{ .id = self.id }; - } }; -pub const IdRef = struct { id: Word }; +pub const IdResultType = IdResult; +pub const IdRef = IdResult; pub const IdMemorySemantics = IdRef; pub const IdScope = IdRef; diff --git a/src/codegen/spirv/type.zig b/src/codegen/spirv/type.zig index dc993b62ff..2e1661c14e 100644 --- a/src/codegen/spirv/type.zig +++ b/src/codegen/spirv/type.zig @@ -3,6 +3,8 @@ const std = @import("std"); const assert = std.debug.assert; +const Signedness = std.builtin.Signedness; +const Allocator = std.mem.Allocator; const spec = @import("spec.zig"); @@ -11,7 +13,7 @@ pub const Type = extern union { ptr_otherwise: *Payload, /// A reference to another SPIR-V type. - pub const Ref = usize; + pub const Ref = enum(u32) { _ }; pub fn initTag(comptime small_tag: Tag) Type { comptime assert(@enumToInt(small_tag) < Tag.no_payload_count); @@ -23,6 +25,41 @@ pub const Type = extern union { return .{ .ptr_otherwise = pl }; } + pub fn int(arena: Allocator, signedness: Signedness, bits: u16) !Type { + const bits_and_signedness = switch (signedness) { + .signed => -@as(i32, bits), + .unsigned => @as(i32, bits), + }; + + return switch (bits_and_signedness) { + 8 => initTag(.u8), + 16 => initTag(.u16), + 32 => initTag(.u32), + 64 => initTag(.u64), + -8 => initTag(.i8), + -16 => initTag(.i16), + -32 => initTag(.i32), + -64 => initTag(.i64), + else => { + const int_payload = try arena.create(Payload.Int); + int_payload.* = .{ + .width = bits, + .signedness = signedness, + }; + return initPayload(&int_payload.base); + }, + }; + } + + pub fn float(bits: u16) Type { + return switch (bits) { + 16 => initTag(.f16), + 32 => initTag(.f32), + 64 => initTag(.f64), + else => unreachable, // Enable more types if required. + }; + } + pub fn tag(self: Type) Tag { if (@enumToInt(self.tag_if_small_enough) < Tag.no_payload_count) { return self.tag_if_small_enough; @@ -80,9 +117,19 @@ pub const Type = extern union { .queue, .pipe_storage, .named_barrier, + .u8, + .u16, + .u32, + .u64, + .i8, + .i16, + .i32, + .i64, + .f16, + .f32, + .f64, => return true, .int, - .float, .vector, .matrix, .sampled_image, @@ -132,6 +179,17 @@ pub const Type = extern union { .queue, .pipe_storage, .named_barrier, + .u8, + .u16, + .u32, + .u64, + .i8, + .i16, + .i32, + .i64, + .f16, + .f32, + .f64, => {}, else => self.hashPayload(@field(Tag, field.name), &hasher), } @@ -185,6 +243,53 @@ pub const Type = extern union { }; } + pub fn isInt(self: Type) bool { + return switch (self.tag()) { + .u8, + .u16, + .u32, + .u64, + .i8, + .i16, + .i32, + .i64, + .int, + => true, + else => false, + }; + } + + pub fn isFloat(self: Type) bool { + return switch (self.tag()) { + .f16, .f32, .f64 => true, + else => false, + }; + } + + /// Returns the number of bits that make up an int or float type. + /// Asserts type is either int or float. + pub fn intFloatBits(self: Type) u16 { + return switch (self.tag()) { + .u8, .i8 => 8, + .u16, .i16, .f16 => 16, + .u32, .i32, .f32 => 32, + .u64, .i64, .f64 => 64, + .int => self.payload(.int).width, + else => unreachable, + }; + } + + /// Returns the signedness of an integer type. + /// Asserts that the type is an int. + pub fn intSignedness(self: Type) Signedness { + return switch (self.tag()) { + .u8, .u16, .u32, .u64 => .unsigned, + .i8, .i16, .i32, .i64 => .signed, + .int => self.payload(.int).signedness, + else => unreachable, + }; + } + pub const Tag = enum(usize) { void, bool, @@ -195,10 +300,20 @@ pub const Type = extern union { queue, pipe_storage, named_barrier, + u8, + u16, + u32, + u64, + i8, + i16, + i32, + i64, + f16, + f32, + f64, // After this, the tag requires a payload. int, - float, vector, matrix, image, @@ -211,14 +326,33 @@ pub const Type = extern union { function, pipe, - pub const last_no_payload_tag = Tag.named_barrier; + pub const last_no_payload_tag = Tag.f64; pub const no_payload_count = @enumToInt(last_no_payload_tag) + 1; pub fn Type(comptime t: Tag) type { return switch (t) { - .void, .bool, .sampler, .event, .device_event, .reserve_id, .queue, .pipe_storage, .named_barrier => @compileError("Type Tag " ++ @tagName(t) ++ " has no payload"), + .void, + .bool, + .sampler, + .event, + .device_event, + .reserve_id, + .queue, + .pipe_storage, + .named_barrier, + .u8, + .u16, + .u32, + .u64, + .i8, + .i16, + .i32, + .i64, + .f16, + .f32, + .f64, + => @compileError("Type Tag " ++ @tagName(t) ++ " has no payload"), .int => Payload.Int, - .float => Payload.Float, .vector => Payload.Vector, .matrix => Payload.Matrix, .image => Payload.Image, @@ -239,13 +373,8 @@ pub const Type = extern union { pub const Int = struct { base: Payload = .{ .tag = .int }, - width: u32, - signedness: std.builtin.Signedness, - }; - - pub const Float = struct { - base: Payload = .{ .tag = .float }, - width: u32, + width: u16, + signedness: Signedness, }; pub const Vector = struct { @@ -292,7 +421,7 @@ pub const Type = extern union { length: u32, /// Type has the 'ArrayStride' decoration. /// If zero, no stride is present. - array_stride: u32, + array_stride: u32 = 0, }; pub const RuntimeArray = struct { @@ -300,35 +429,39 @@ pub const Type = extern union { element_type: Ref, /// Type has the 'ArrayStride' decoration. /// If zero, no stride is present. - array_stride: u32, + array_stride: u32 = 0, }; pub const Struct = struct { base: Payload = .{ .tag = .@"struct" }, members: []Member, - decorations: StructDecorations, + name: []const u8 = "", + decorations: StructDecorations = .{}, /// Extra information for decorations, packed for efficiency. Fields are stored sequentially by /// order of the `members` slice and `MemberDecorations` struct. - member_decoration_extra: []u32, + member_decoration_extra: []u32 = &.{}, pub const Member = struct { ty: Ref, - offset: u32, - decorations: MemberDecorations, + name: []const u8 = "", + offset: MemberOffset = .none, + decorations: MemberDecorations = .{}, }; + pub const MemberOffset = enum(u32) { none = 0xFFFF_FFFF, _ }; + pub const StructDecorations = packed struct { /// Type has the 'Block' decoration. - block: bool, + block: bool = false, /// Type has the 'BufferBlock' decoration. - buffer_block: bool, + buffer_block: bool = false, /// Type has the 'GLSLShared' decoration. - glsl_shared: bool, + glsl_shared: bool = false, /// Type has the 'GLSLPacked' decoration. - glsl_packed: bool, + glsl_packed: bool = false, /// Type has the 'CPacked' decoration. - c_packed: bool, + c_packed: bool = false, }; pub const MemberDecorations = packed struct { @@ -344,31 +477,31 @@ pub const Type = extern union { col_major, /// Member is not a matrix or array of matrices. none, - }, + } = .none, // Regular decorations, these do not imply extra fields. /// Member has the 'NoPerspective' decoration. - no_perspective: bool, + no_perspective: bool = false, /// Member has the 'Flat' decoration. - flat: bool, + flat: bool = false, /// Member has the 'Patch' decoration. - patch: bool, + patch: bool = false, /// Member has the 'Centroid' decoration. - centroid: bool, + centroid: bool = false, /// Member has the 'Sample' decoration. - sample: bool, + sample: bool = false, /// Member has the 'Invariant' decoration. /// Note: requires parent struct to have 'Block'. - invariant: bool, + invariant: bool = false, /// Member has the 'Volatile' decoration. - @"volatile": bool, + @"volatile": bool = false, /// Member has the 'Coherent' decoration. - coherent: bool, + coherent: bool = false, /// Member has the 'NonWritable' decoration. - non_writable: bool, + non_writable: bool = false, /// Member has the 'NonReadable' decoration. - non_readable: bool, + non_readable: bool = false, // The following decorations all imply extra field(s). @@ -377,27 +510,27 @@ pub const Type = extern union { /// Note: If any member of a struct has the BuiltIn decoration, all members must have one. /// Note: Each builtin may only be reachable once for a particular entry point. /// Note: The member type may be constrained by a particular built-in, defined in the client API specification. - builtin: bool, + builtin: bool = false, /// Member has the 'Stream' decoration. /// This member has an extra field of type `u32`. - stream: bool, + stream: bool = false, /// Member has the 'Location' decoration. /// This member has an extra field of type `u32`. - location: bool, + location: bool = false, /// Member has the 'Component' decoration. /// This member has an extra field of type `u32`. - component: bool, + component: bool = false, /// Member has the 'XfbBuffer' decoration. /// This member has an extra field of type `u32`. - xfb_buffer: bool, + xfb_buffer: bool = false, /// Member has the 'XfbStride' decoration. /// This member has an extra field of type `u32`. - xfb_stride: bool, + xfb_stride: bool = false, /// Member has the 'UserSemantic' decoration. /// This member has an extra field of type `[]u8`, which is encoded /// by an `u32` containing the number of chars exactly, and then the string padded to /// a multiple of 4 bytes with zeroes. - user_semantic: bool, + user_semantic: bool = false, }; }; @@ -413,11 +546,11 @@ pub const Type = extern union { /// Type has the 'ArrayStride' decoration. /// This is valid for pointers to elements of an array. /// If zero, no stride is present. - array_stride: u32, - /// Type has the 'Alignment' decoration. - alignment: ?u32, + array_stride: u32 = 0, + /// If nonzero, type has the 'Alignment' decoration. + alignment: u32 = 0, /// Type has the 'MaxByteOffset' decoration. - max_byte_offset: ?u32, + max_byte_offset: ?u32 = null, }; pub const Function = struct { diff --git a/src/link/NvPtx.zig b/src/link/NvPtx.zig index 4c6c6fd8be..c542241cd9 100644 --- a/src/link/NvPtx.zig +++ b/src/link/NvPtx.zig @@ -1,7 +1,7 @@ //! NVidia PTX (Paralle Thread Execution) //! https://docs.nvidia.com/cuda/parallel-thread-execution/index.html //! For this we rely on the nvptx backend of LLVM -//! Kernel functions need to be marked both as "export" and "callconv(.PtxKernel)" +//! Kernel functions need to be marked both as "export" and "callconv(.Kernel)" const NvPtx = @This(); diff --git a/src/link/SpirV.zig b/src/link/SpirV.zig index 2d74e404eb..fbdcbd5a8e 100644 --- a/src/link/SpirV.zig +++ b/src/link/SpirV.zig @@ -44,34 +44,25 @@ const IdResult = spec.IdResult; base: link.File, -/// This linker backend does not try to incrementally link output SPIR-V code. -/// Instead, it tracks all declarations in this table, and iterates over it -/// in the flush function. -decl_table: std.AutoArrayHashMapUnmanaged(Module.Decl.Index, DeclGenContext) = .{}, - -const DeclGenContext = struct { - air: Air, - air_arena: ArenaAllocator.State, - liveness: Liveness, - - fn deinit(self: *DeclGenContext, gpa: Allocator) void { - self.air.deinit(gpa); - self.liveness.deinit(gpa); - self.air_arena.promote(gpa).deinit(); - self.* = undefined; - } -}; +spv: SpvModule, +spv_arena: ArenaAllocator, +decl_link: codegen.DeclLinkMap, pub fn createEmpty(gpa: Allocator, options: link.Options) !*SpirV { - const spirv = try gpa.create(SpirV); - spirv.* = .{ + const self = try gpa.create(SpirV); + self.* = .{ .base = .{ .tag = .spirv, .options = options, .file = null, .allocator = gpa, }, + .spv = undefined, + .spv_arena = ArenaAllocator.init(gpa), + .decl_link = codegen.DeclLinkMap.init(self.base.allocator), }; + self.spv = SpvModule.init(gpa, self.spv_arena.allocator()); + errdefer self.deinit(); // TODO: Figure out where to put all of these switch (options.target.cpu.arch) { @@ -88,7 +79,7 @@ pub fn createEmpty(gpa: Allocator, options: link.Options) !*SpirV { return error.TODOAbiNotSupported; } - return spirv; + return self; } pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Options) !*SpirV { @@ -107,44 +98,35 @@ pub fn openPath(allocator: Allocator, sub_path: []const u8, options: link.Option } pub fn deinit(self: *SpirV) void { - self.decl_table.deinit(self.base.allocator); + self.spv.deinit(); + self.spv_arena.deinit(); + self.decl_link.deinit(); } pub fn updateFunc(self: *SpirV, module: *Module, func: *Module.Fn, air: Air, liveness: Liveness) !void { if (build_options.skip_non_native) { @panic("Attempted to compile for architecture that was disabled by build configuration"); } - _ = module; - // Keep track of all decls so we can iterate over them on flush(). - const result = try self.decl_table.getOrPut(self.base.allocator, func.owner_decl); - if (result.found_existing) { - result.value_ptr.deinit(self.base.allocator); + var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &self.spv, &self.decl_link); + defer decl_gen.deinit(); + + if (try decl_gen.gen(func.owner_decl, air, liveness)) |msg| { + try module.failed_decls.put(module.gpa, func.owner_decl, msg); } - - var arena = ArenaAllocator.init(self.base.allocator); - errdefer arena.deinit(); - - var new_air = try cloneAir(air, self.base.allocator, arena.allocator()); - errdefer new_air.deinit(self.base.allocator); - - var new_liveness = try cloneLiveness(liveness, self.base.allocator); - errdefer new_liveness.deinit(self.base.allocator); - - result.value_ptr.* = .{ - .air = new_air, - .air_arena = arena.state, - .liveness = new_liveness, - }; } pub fn updateDecl(self: *SpirV, module: *Module, decl_index: Module.Decl.Index) !void { if (build_options.skip_non_native) { @panic("Attempted to compile for architecture that was disabled by build configuration"); } - _ = module; - // Keep track of all decls so we can iterate over them on flush(). - _ = try self.decl_table.getOrPut(self.base.allocator, decl_index); + + var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &self.spv, &self.decl_link); + defer decl_gen.deinit(); + + if (try decl_gen.gen(decl_index, undefined, undefined)) |msg| { + try module.failed_decls.put(module.gpa, decl_index, msg); + } } pub fn updateDeclExports( @@ -153,20 +135,26 @@ pub fn updateDeclExports( decl_index: Module.Decl.Index, exports: []const *Module.Export, ) !void { - _ = self; - _ = module; - _ = decl_index; - _ = exports; + const decl = module.declPtr(decl_index); + if (decl.val.tag() == .function and decl.ty.fnCallingConvention() == .Kernel) { + // TODO: Unify with resolveDecl in spirv.zig. + const entry = try self.decl_link.getOrPut(decl_index); + if (!entry.found_existing) { + entry.value_ptr.* = try self.spv.allocDecl(.func); + } + const spv_decl_index = entry.value_ptr.*; + + for (exports) |exp| { + try self.spv.declareEntryPoint(spv_decl_index, exp.options.name); + } + } + + // TODO: Export regular functions, variables, etc using Linkage attributes. } pub fn freeDecl(self: *SpirV, decl_index: Module.Decl.Index) void { - if (self.decl_table.getIndex(decl_index)) |index| { - const module = self.base.options.module.?; - const decl = module.declPtr(decl_index); - if (decl.val.tag() == .function) { - self.decl_table.values()[index].deinit(self.base.allocator); - } - } + _ = self; + _ = decl_index; } pub fn flush(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.Node) link.File.FlushError!void { @@ -189,60 +177,38 @@ pub fn flushModule(self: *SpirV, comp: *Compilation, prog_node: *std.Progress.No sub_prog_node.activate(); defer sub_prog_node.end(); - const module = self.base.options.module.?; const target = comp.getTarget(); + try writeCapabilities(&self.spv, target); + try writeMemoryModel(&self.spv, target); - var arena = std.heap.ArenaAllocator.init(self.base.allocator); - defer arena.deinit(); + // We need to export the list of error names somewhere so that we can pretty-print them in the + // executor. This is not really an important thing though, so we can just dump it in any old + // nonsemantic instruction. For now, just put it in OpSourceExtension with a special name. - var spv = SpvModule.init(self.base.allocator, arena.allocator()); - defer spv.deinit(); + var error_info = std.ArrayList(u8).init(self.spv.arena); + try error_info.appendSlice("zig_errors"); + const module = self.base.options.module.?; + for (module.error_name_list.items) |name| { + // Errors can contain pretty much any character - to encode them in a string we must escape + // them somehow. Easiest here is to use some established scheme, one which also preseves the + // name if it contains no strange characters is nice for debugging. URI encoding fits the bill. + // We're using : as separator, which is a reserved character. - // Allocate an ID for every declaration before generating code, - // so that we can access them before processing them. - // TODO: We're allocating an ID unconditionally now, are there - // declarations which don't generate a result? - var ids = std.AutoHashMap(Module.Decl.Index, IdResult).init(self.base.allocator); - defer ids.deinit(); - try ids.ensureTotalCapacity(@intCast(u32, self.decl_table.count())); - - for (self.decl_table.keys()) |decl_index| { - const decl = module.declPtr(decl_index); - if (decl.has_tv) { - ids.putAssumeCapacityNoClobber(decl_index, spv.allocId()); - } + const escaped_name = try std.Uri.escapeString(self.base.allocator, name); + defer self.base.allocator.free(escaped_name); + try error_info.writer().print(":{s}", .{escaped_name}); } + try self.spv.sections.debug_strings.emit(self.spv.gpa, .OpSourceExtension, .{ + .extension = error_info.items, + }); - // Now, actually generate the code for all declarations. - var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &spv, &ids); - defer decl_gen.deinit(); - - var it = self.decl_table.iterator(); - while (it.next()) |entry| { - const decl_index = entry.key_ptr.*; - const decl = module.declPtr(decl_index); - if (!decl.has_tv) continue; - - const air = entry.value_ptr.air; - const liveness = entry.value_ptr.liveness; - - // Note, if `decl` is not a function, air/liveness may be undefined. - if (try decl_gen.gen(decl_index, air, liveness)) |msg| { - try module.failed_decls.put(module.gpa, decl_index, msg); - return; // TODO: Attempt to generate more decls? - } - } - - try writeCapabilities(&spv, target); - try writeMemoryModel(&spv, target); - - try spv.flush(self.base.file.?); + try self.spv.flush(self.base.file.?); } fn writeCapabilities(spv: *SpvModule, target: std.Target) !void { // TODO: Integrate with a hypothetical feature system const caps: []const spec.Capability = switch (target.os.tag) { - .opencl => &.{.Kernel}, + .opencl => &.{ .Kernel, .Addresses, .Int8, .Int16, .Int64, .GenericPointer }, .glsl450 => &.{.Shader}, .vulkan => &.{.Shader}, else => unreachable, // TODO @@ -279,45 +245,3 @@ fn writeMemoryModel(spv: *SpvModule, target: std.Target) !void { .memory_model = memory_model, }); } - -fn cloneLiveness(l: Liveness, gpa: Allocator) !Liveness { - const tomb_bits = try gpa.dupe(usize, l.tomb_bits); - errdefer gpa.free(tomb_bits); - - const extra = try gpa.dupe(u32, l.extra); - errdefer gpa.free(extra); - - return Liveness{ - .tomb_bits = tomb_bits, - .extra = extra, - .special = try l.special.clone(gpa), - }; -} - -fn cloneAir(air: Air, gpa: Allocator, air_arena: Allocator) !Air { - const values = try gpa.alloc(Value, air.values.len); - errdefer gpa.free(values); - - for (values, 0..) |*value, i| { - value.* = try air.values[i].copy(air_arena); - } - - var instructions = try air.instructions.toMultiArrayList().clone(gpa); - errdefer instructions.deinit(gpa); - - const air_tags = instructions.items(.tag); - const air_datas = instructions.items(.data); - - for (air_tags, 0..) |tag, i| { - switch (tag) { - .alloc, .ret_ptr, .const_ty => air_datas[i].ty = try air_datas[i].ty.copy(air_arena), - else => {}, - } - } - - return Air{ - .instructions = instructions.slice(), - .extra = try gpa.dupe(u32, air.extra), - .values = values, - }; -} diff --git a/src/target.zig b/src/target.zig index f785082bd4..ad3e0905eb 100644 --- a/src/target.zig +++ b/src/target.zig @@ -163,7 +163,7 @@ pub fn canBuildLibC(target: std.Target) bool { pub fn cannotDynamicLink(target: std.Target) bool { return switch (target.os.tag) { .freestanding, .other => true, - else => false, + else => target.isSpirV(), }; } @@ -331,18 +331,18 @@ pub fn supportsStackProbing(target: std.Target) bool { } pub fn supportsStackProtector(target: std.Target) bool { - _ = target; - return true; + return !target.isSpirV(); } pub fn libcProvidesStackProtector(target: std.Target) bool { - return !target.isMinGW() and target.os.tag != .wasi; + return !target.isMinGW() and target.os.tag != .wasi and !target.isSpirV(); } pub fn supportsReturnAddress(target: std.Target) bool { return switch (target.cpu.arch) { .wasm32, .wasm64 => target.os.tag == .emscripten, .bpfel, .bpfeb => false, + .spirv32, .spirv64 => false, else => true, }; } diff --git a/src/type.zig b/src/type.zig index 15525f14eb..9f1905b1ad 100644 --- a/src/type.zig +++ b/src/type.zig @@ -4796,9 +4796,12 @@ pub const Type = extern union { } /// Asserts the type is a function. - pub fn fnCallingConventionAllowsZigTypes(cc: std.builtin.CallingConvention) bool { + pub fn fnCallingConventionAllowsZigTypes(target: Target, cc: std.builtin.CallingConvention) bool { return switch (cc) { - .Unspecified, .Async, .Inline, .PtxKernel => true, + .Unspecified, .Async, .Inline => true, + // For now we want to authorize PTX kernel to use zig objects, even if we end up exposing the ABI. + // The goal is to experiment with more integrated CPU/GPU code. + .Kernel => target.cpu.arch == .nvptx or target.cpu.arch == .nvptx64, else => false, }; } diff --git a/test/nvptx.zig b/test/nvptx.zig index 57853a657d..0bdc9455f7 100644 --- a/test/nvptx.zig +++ b/test/nvptx.zig @@ -10,7 +10,7 @@ pub fn addCases(ctx: *Cases) !void { \\ return a + b; \\} \\ - \\pub export fn add_and_substract(a: i32, out: *i32) callconv(.PtxKernel) void { + \\pub export fn add_and_substract(a: i32, out: *i32) callconv(.Kernel) void { \\ const x = add(a, 7); \\ var y = add(2, 0); \\ y -= x; @@ -29,7 +29,7 @@ pub fn addCases(ctx: *Cases) !void { \\ ); \\} \\ - \\pub export fn special_reg(a: []const i32, out: []i32) callconv(.PtxKernel) void { + \\pub export fn special_reg(a: []const i32, out: []i32) callconv(.Kernel) void { \\ const i = threadIdX(); \\ out[i] = a[i] + 7; \\} @@ -42,7 +42,7 @@ pub fn addCases(ctx: *Cases) !void { case.addCompile( \\var x: i32 addrspace(.global) = 0; \\ - \\pub export fn increment(out: *i32) callconv(.PtxKernel) void { + \\pub export fn increment(out: *i32) callconv(.Kernel) void { \\ x += 1; \\ out.* = x; \\} @@ -59,7 +59,7 @@ pub fn addCases(ctx: *Cases) !void { \\} \\ \\ var _sdata: [1024]f32 addrspace(.shared) = undefined; - \\ pub export fn reduceSum(d_x: []const f32, out: *f32) callconv(.PtxKernel) void { + \\ pub export fn reduceSum(d_x: []const f32, out: *f32) callconv(.Kernel) void { \\ var sdata = @addrSpaceCast(.generic, &_sdata); \\ const tid: u32 = threadIdX(); \\ var sum = d_x[tid]; diff --git a/tools/gen_spirv_spec.zig b/tools/gen_spirv_spec.zig index 31dbbb1911..5ed76448e4 100644 --- a/tools/gen_spirv_spec.zig +++ b/tools/gen_spirv_spec.zig @@ -80,22 +80,11 @@ fn render(writer: anytype, allocator: Allocator, registry: g.CoreRegistry) !void \\const Version = @import("std").builtin.Version; \\ \\pub const Word = u32; - \\pub const IdResultType = struct{ - \\ id: Word, - \\ pub fn toRef(self: IdResultType) IdRef { - \\ return .{.id = self.id}; - \\ } - \\}; \\pub const IdResult = struct{ \\ id: Word, - \\ pub fn toRef(self: IdResult) IdRef { - \\ return .{.id = self.id}; - \\ } - \\ pub fn toResultType(self: IdResult) IdResultType { - \\ return .{.id = self.id}; - \\ } \\}; - \\pub const IdRef = struct{ id: Word }; + \\pub const IdResultType = IdResult; + \\pub const IdRef = IdResult; \\ \\pub const IdMemorySemantics = IdRef; \\pub const IdScope = IdRef;