From c6d178f93de5922163b51cedbf05023623558c6b Mon Sep 17 00:00:00 2001 From: Ali Cheraghi Date: Sun, 14 Jun 2026 19:48:11 +0330 Subject: [PATCH] spirv: make codegen multi-threaded The SPIR-V backend previously ran codegen single-threaded in the linker thread. Now each codegen job creates an `Mir` like other self-hosted backends. Other changes: - Bring back `dedup_types.zig` and `prune_unused.zig` **ISel**s which were originaly removed because codegen was single-threaded at that time and therefor had no use - Clean up `BinaryModule.zig` - Remove `checkLogicalPtrOperation` from `elemPtrOneLayerOnly` in `Sema.zig`. Element access uses `OpAccessChain`, which works on all logical address spaces without `VariablePointers`. the check is only needed for pointer arithmetic. --- src/Sema.zig | 30 +- src/Zcu/PerThread.zig | 13 - src/codegen.zig | 4 + src/codegen/spirv/CodeGen.zig | 194 +++++ src/codegen/spirv/Mir.zig | 71 ++ src/link.zig | 1 - src/link/SpirV.zig | 727 +++++++++++++++--- src/link/SpirV/BinaryModule.zig | 281 ++----- src/link/SpirV/dedup_types.zig | 255 ++++++ src/link/SpirV/lower_invocation_globals.zig | 36 +- src/link/SpirV/prune_unused.zig | 234 ++++++ src/target.zig | 4 +- test/behavior/align.zig | 2 + test/behavior/enum.zig | 2 + test/behavior/inline_switch.zig | 7 + test/behavior/ir_block_deps.zig | 1 + test/behavior/switch.zig | 1 + test/behavior/tuple.zig | 1 + test/behavior/union.zig | 3 + .../illegal_operation_on_logical_ptr.zig | 32 +- 20 files changed, 1512 insertions(+), 387 deletions(-) create mode 100644 src/codegen/spirv/Mir.zig create mode 100644 src/link/SpirV/dedup_types.zig create mode 100644 src/link/SpirV/prune_unused.zig diff --git a/src/Sema.zig b/src/Sema.zig index f6e05f2c10c6bc4ad6849b04faf783d53c484666..7f6adfd5d486be772dac092f79f1e02d16a7e314 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -27094,11 +27094,9 @@ fn elemPtrOneLayerOnly( return .fromValue(try ptr_val.ptrElem(index, pt)); } - try sema.checkLogicalPtrOperation(block, src, indexable_ty); - const result_ty = try indexable_ty.elemPtrType(maybe_index, pt); - try sema.validateRuntimeElemAccess(block, elem_index_src, result_ty, indexable_ty, indexable_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, result_ty, indexable_src); try sema.validateRuntimeValue(block, indexable_src, indexable); if (child_ty.abiSize(zcu) == 0) { @@ -27161,7 +27159,7 @@ fn elemVal( return sema.analyzeLoad(block, src, .fromValue(elem_ptr_val), indexable_src); } - try sema.validateRuntimeElemAccess(block, elem_index_src, child_ty, indexable_ty, src); + try sema.validateRuntimeElemAccess(block, elem_index_src, child_ty, src); switch (child_ty.classify(zcu)) { .runtime => {}, .one_possible_value => return .fromValue((try child_ty.onePossibleValue(pt)).?), @@ -27210,11 +27208,9 @@ fn validateRuntimeElemAccess( block: *Block, elem_index_src: LazySrcLoc, elem_ty: Type, - parent_ty: Type, parent_src: LazySrcLoc, ) CompileError!void { - const pt = sema.pt; - const zcu = pt.zcu; + const zcu = sema.pt.zcu; if (elem_ty.comptimeOnly(zcu)) { const msg = msg: { @@ -27231,14 +27227,6 @@ fn validateRuntimeElemAccess( }; return sema.failWithOwnedErrorMsg(block, msg); } - - if (zcu.intern_pool.indexToKey(parent_ty.toIntern()) == .ptr_type) { - const target = zcu.getTarget(); - const as = parent_ty.ptrAddressSpace(zcu); - if (target_util.shouldBlockPointerOps(target, as)) { - return sema.fail(block, elem_index_src, "cannot access element of logical pointer '{f}'", .{parent_ty.fmt(pt)}); - } - } } /// Validates `elem_index`, and returns a pointer to that field using `structFieldPtrByIndex`. @@ -27309,7 +27297,7 @@ fn tupleField( return Air.internedToRef((try tuple_val.fieldValue(pt, field_index)).toIntern()); } - try sema.validateRuntimeElemAccess(block, field_index_src, field_ty, tuple_ty, tuple_src); + try sema.validateRuntimeElemAccess(block, field_index_src, field_ty, tuple_src); return block.addStructFieldVal(tuple, field_index, field_ty); } @@ -27364,7 +27352,7 @@ fn elemValArray( if (try elem_ty.onePossibleValue(pt)) |opv| return .fromValue(opv); } - try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, array_ty, array_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, array_src); try sema.validateRuntimeValue(block, array_src, array); if (oob_safety and block.wantSafety()) { @@ -27467,7 +27455,7 @@ fn elemPtrVector( }; if (!init) { - try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, vector_ty, vector_ptr_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, vector_ptr_src); try sema.validateRuntimeValue(block, vector_ptr_src, vector_ptr); } @@ -27540,7 +27528,7 @@ fn elemPtrArray( } if (!init) { - try sema.validateRuntimeElemAccess(block, elem_index_src, array_ty.childType(zcu), array_ty, array_ptr_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, array_ty.childType(zcu), array_ptr_src); try sema.validateRuntimeValue(block, array_ptr_src, array_ptr); } @@ -27603,7 +27591,7 @@ fn elemValSlice( if (try elem_ty.onePossibleValue(pt)) |opv| return .fromValue(opv); - try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, slice_ty, slice_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ty, slice_src); try sema.validateRuntimeValue(block, slice_src, slice); if (oob_safety and block.wantSafety()) { @@ -27663,7 +27651,7 @@ fn elemPtrSlice( } } - try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ptr_ty, slice_ty, slice_src); + try sema.validateRuntimeElemAccess(block, elem_index_src, elem_ptr_ty, slice_src); try sema.validateRuntimeValue(block, slice_src, slice); if (oob_safety and block.wantSafety()) { diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index 8428627bc51ceaaf3eea7b5845c7570758bdede3..c9e6eedc6d7ddb000b80c29b22b6a45abfb1234c 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -4513,7 +4513,6 @@ pub fn runCodegen(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) Ru comp.config.use_llvm, )) { else => unreachable, // assertion failure - .stage2_spirv, .stage2_llvm, => {}, }, @@ -4594,18 +4593,6 @@ fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) e const lf = comp.bin_file orelse return error.NoLinkFile; - // Just like LLVM, the SPIR-V backend can't multi-threaded due to SPIR-V design limitations. - if (lf.cast(.spirv)) |spirv_file| { - assert(zcu.pending_codegen_jobs.load(.monotonic) == 2); // only one codegen at a time (but the value is 2 because 1 is the base) - spirv_file.updateFunc(pt, func_index, air, &liveness) catch |err| { - switch (err) { - error.OutOfMemory => comp.link_diags.setAllocFailure(), - } - return error.AlreadyReported; - }; - return error.BackendDoesNotProduceMir; - } - return codegen.generateFunction(lf, pt, func_index, air, &liveness); } diff --git a/src/codegen.zig b/src/codegen.zig index ee20f58f8feb24d3f55662fcf4e03a2d52e7a9c5..55d73b476a7a9a9a89a18fc4c8be9b8e139ac6c3 100644 --- a/src/codegen.zig +++ b/src/codegen.zig @@ -101,6 +101,7 @@ pub const AnyMir = union { x86_64: if (dev.env.supports(.x86_64_backend)) @import("codegen/x86_64/Mir.zig") else noreturn, wasm: if (dev.env.supports(.wasm_backend)) @import("codegen/wasm/Mir.zig") else noreturn, c: if (dev.env.supports(.c_backend)) @import("codegen/c.zig").Mir else noreturn, + spirv: if (dev.env.supports(.spirv_backend)) @import("codegen/spirv/Mir.zig") else noreturn, pub inline fn tag(comptime backend: std.lang.CompilerBackend) []const u8 { return switch (backend) { @@ -110,6 +111,7 @@ pub const AnyMir = union { .stage2_x86_64 => "x86_64", .stage2_wasm => "wasm", .stage2_c => "c", + .stage2_spirv => "spirv", else => unreachable, }; } @@ -125,6 +127,7 @@ pub const AnyMir = union { .stage2_x86_64, .stage2_wasm, .stage2_c, + .stage2_spirv, => |backend_ct| @field(mir, tag(backend_ct)).deinit(gpa), } } @@ -153,6 +156,7 @@ pub fn generateFunction( .stage2_x86_64, .stage2_wasm, .stage2_c, + .stage2_spirv, => |backend| { dev.check(devFeatureForBackend(backend)); const CodeGen = importBackend(backend); diff --git a/src/codegen/spirv/CodeGen.zig b/src/codegen/spirv/CodeGen.zig index 4aa82e1740feaf6e4d1094a6141311a207d6764c..c8f9df44f40976ff780e720a947342f4f46b14c3 100644 --- a/src/codegen/spirv/CodeGen.zig +++ b/src/codegen/spirv/CodeGen.zig @@ -5,6 +5,8 @@ const Signedness = std.lang.Signedness; const assert = std.debug.assert; const log = std.log.scoped(.codegen); +const link = @import("../../link.zig"); +const codegen = @import("../../codegen.zig"); const Zcu = @import("../../Zcu.zig"); const Type = @import("../../Type.zig"); const Value = @import("../../Value.zig"); @@ -12,6 +14,7 @@ const Air = @import("../../Air.zig"); const InternPool = @import("../../InternPool.zig"); const Section = @import("Section.zig"); const Assembler = @import("Assembler.zig"); +const Mir = @import("Mir.zig"); const spec = @import("spec.zig"); const Opcode = spec.Opcode; @@ -169,6 +172,197 @@ pub fn deinit(cg: *CodeGen) void { cg.body.deinit(gpa); } +pub fn generate( + _: *link.File, + pt: Zcu.PerThread, + func_index: InternPool.Index, + air: *const Air, + liveness: *const ?Air.Liveness, +) codegen.Error!Mir { + const zcu = pt.zcu; + const gpa = zcu.gpa; + const nav = zcu.funcInfo(func_index).owner_nav; + const structured_cfg = zcu.navFileScope(nav).mod.?.structured_cfg; + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + var module: Module = .{ + .gpa = gpa, + .arena = arena.allocator(), + .zcu = zcu, + }; + defer module.deinit(); + + var cg: CodeGen = .{ + .pt = pt, + .air = air.*, + .liveness = liveness.*.?, + .owner_nav = nav, + .module = &module, + .control_flow = switch (structured_cfg) { + true => .{ .structured = .{} }, + false => .{ .unstructured = .{} }, + }, + .base_line = zcu.navSrcLine(nav), + }; + defer cg.deinit(); + + cg.genNav(true) catch |err| switch (err) { + error.AlreadyReported => return error.AlreadyReported, + error.OutOfMemory => return error.OutOfMemory, + }; + + return cg.serializeToMir(gpa); +} + +pub fn generateNav( + pt: Zcu.PerThread, + nav_index: InternPool.Nav.Index, +) codegen.Error!Mir { + const zcu = pt.zcu; + const gpa = zcu.gpa; + const structured_cfg = zcu.navFileScope(nav_index).mod.?.structured_cfg; + + var arena = std.heap.ArenaAllocator.init(gpa); + defer arena.deinit(); + var module: Module = .{ + .gpa = gpa, + .arena = arena.allocator(), + .zcu = zcu, + }; + defer module.deinit(); + + var cg: CodeGen = .{ + .pt = pt, + .air = undefined, + .liveness = undefined, + .owner_nav = nav_index, + .module = &module, + .control_flow = switch (structured_cfg) { + true => .{ .structured = .{} }, + false => .{ .unstructured = .{} }, + }, + .base_line = zcu.navSrcLine(nav_index), + }; + defer cg.deinit(); + + cg.genNav(false) catch |err| switch (err) { + error.AlreadyReported => return error.AlreadyReported, + error.OutOfMemory => return error.OutOfMemory, + }; + + return cg.serializeToMir(gpa); +} + +fn serializeToMir(cg: *CodeGen, gpa: Allocator) codegen.Error!Mir { + const module = cg.module; + + const owner_entry = module.nav_link.get(cg.owner_nav); + const owner_decl_index = owner_entry orelse return .{ + .id_bound = module.next_result_id, + .owner_nav = cg.owner_nav, + .kind = .func, + .decl_result_id = .none, + .extended_instruction_set = &.{}, + .globals = &.{}, + .functions = &.{}, + .annotations = &.{}, + .debug_names = &.{}, + .debug_strings = &.{}, + .execution_modes = &.{}, + .nav_refs = &.{}, + .uav_refs = &.{}, + .decl_deps = &.{}, + .internal_globals = &.{}, + .entry_points = &.{}, + }; + + const owner_decl = module.declPtr(owner_decl_index); + + var nav_refs: std.ArrayList(Mir.NavRef) = .empty; + defer nav_refs.deinit(gpa); + var nav_it = module.nav_link.iterator(); + while (nav_it.next()) |entry| { + if (entry.key_ptr.* == cg.owner_nav) continue; + const decl = module.declPtr(entry.value_ptr.*); + try nav_refs.append(gpa, .{ + .local_id = decl.result_id, + .nav = entry.key_ptr.*, + .kind = decl.kind, + }); + } + + var uav_refs: std.ArrayList(Mir.UavRef) = .empty; + defer uav_refs.deinit(gpa); + var uav_it = module.uav_link.iterator(); + while (uav_it.next()) |entry| { + const decl = module.declPtr(entry.value_ptr.*); + try uav_refs.append(gpa, .{ + .local_id = decl.result_id, + .val = entry.key_ptr.*[0], + .storage_class = entry.key_ptr.*[1], + .kind = decl.kind, + }); + } + + var decl_deps: std.ArrayList(Mir.DeclDep) = .empty; + defer decl_deps.deinit(gpa); + var internal_globals: std.ArrayList(Id) = .empty; + defer internal_globals.deinit(gpa); + + const deps = module.decl_deps.items[owner_decl.begin_dep..owner_decl.end_dep]; + for (deps) |dep_index| { + const dep_decl = module.declPtr(dep_index); + var found = false; + nav_it.index = 0; + while (nav_it.next()) |entry| { + if (entry.value_ptr.* == dep_index) { + try decl_deps.append(gpa, .{ + .kind = dep_decl.kind, + .nav = entry.key_ptr.*, + }); + found = true; + break; + } + } + if (!found and dep_decl.kind == .global) { + try internal_globals.append(gpa, dep_decl.result_id); + } + } + + var ep_list: std.ArrayList(Mir.EntryPoint) = .empty; + defer ep_list.deinit(gpa); + var ep_it = module.entry_points.iterator(); + while (ep_it.next()) |entry| { + const ep = entry.value_ptr; + const ep_decl = module.declPtr(ep.decl_index); + try ep_list.append(gpa, .{ + .local_id = ep_decl.result_id, + .name = try gpa.dupe(u8, ep.name), + .cc = ep.cc, + }); + } + + return .{ + .id_bound = module.next_result_id, + .owner_nav = cg.owner_nav, + .kind = owner_decl.kind, + .decl_result_id = owner_decl.result_id, + .extended_instruction_set = try module.sections.extended_instruction_set.instructions.toOwnedSlice(gpa), + .globals = try module.sections.globals.instructions.toOwnedSlice(gpa), + .functions = try module.sections.functions.instructions.toOwnedSlice(gpa), + .annotations = try module.sections.annotations.instructions.toOwnedSlice(gpa), + .debug_names = try module.sections.debug_names.instructions.toOwnedSlice(gpa), + .debug_strings = try module.sections.debug_strings.instructions.toOwnedSlice(gpa), + .execution_modes = try module.sections.execution_modes.instructions.toOwnedSlice(gpa), + .nav_refs = try nav_refs.toOwnedSlice(gpa), + .uav_refs = try uav_refs.toOwnedSlice(gpa), + .decl_deps = try decl_deps.toOwnedSlice(gpa), + .internal_globals = try internal_globals.toOwnedSlice(gpa), + .entry_points = try ep_list.toOwnedSlice(gpa), + }; +} + const Error = error{ AlreadyReported, OutOfMemory }; pub fn genNav(cg: *CodeGen, do_codegen: bool) Error!void { diff --git a/src/codegen/spirv/Mir.zig b/src/codegen/spirv/Mir.zig new file mode 100644 index 0000000000000000000000000000000000000000..82fc36ab370d434e151956aa2660b83c8c6652d4 --- /dev/null +++ b/src/codegen/spirv/Mir.zig @@ -0,0 +1,71 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const spec = @import("spec.zig"); +const Word = spec.Word; +const Id = spec.Id; + +const InternPool = @import("../../InternPool.zig"); +const Module = @import("Module.zig"); + +const Mir = @This(); + +id_bound: Word, +owner_nav: InternPool.Nav.Index, +kind: Module.Decl.Kind, +decl_result_id: Id, +extended_instruction_set: []const Word, +globals: []const Word, +functions: []const Word, +annotations: []const Word, +debug_names: []const Word, +debug_strings: []const Word, +execution_modes: []const Word, +nav_refs: []const NavRef, +uav_refs: []const UavRef, +decl_deps: []const DeclDep, +internal_globals: []const Id, +entry_points: []const EntryPoint, + +pub const NavRef = struct { + local_id: Id, + nav: InternPool.Nav.Index, + kind: Module.Decl.Kind, +}; + +pub const UavRef = struct { + local_id: Id, + val: InternPool.Index, + storage_class: spec.StorageClass, + kind: Module.Decl.Kind, +}; + +pub const DeclDep = struct { + kind: Module.Decl.Kind, + nav: InternPool.Nav.Index, +}; + +pub const EntryPoint = struct { + local_id: Id, + name: []const u8, + cc: std.builtin.CallingConvention, +}; + +pub fn deinit(mir: *Mir, gpa: Allocator) void { + gpa.free(mir.extended_instruction_set); + gpa.free(mir.globals); + gpa.free(mir.functions); + gpa.free(mir.annotations); + gpa.free(mir.debug_names); + gpa.free(mir.debug_strings); + gpa.free(mir.execution_modes); + gpa.free(mir.nav_refs); + gpa.free(mir.uav_refs); + gpa.free(mir.decl_deps); + gpa.free(mir.internal_globals); + for (mir.entry_points) |ep| { + gpa.free(ep.name); + } + gpa.free(mir.entry_points); + mir.* = undefined; +} diff --git a/src/link.zig b/src/link.zig index 59a8ea84d86e14b1d8f880f52dfa4565e85f6c21..47470b5a6994cb2564cb70860f5b041f32e3da22 100644 --- a/src/link.zig +++ b/src/link.zig @@ -841,7 +841,6 @@ pub const File = struct { assert(base.comp.zcu.?.llvm_object == null); switch (base.tag) { .lld => unreachable, - .spirv => unreachable, // see corresponding special case in `Zcu.PerThread.runCodegenInner` .plan9 => unreachable, inline else => |tag| { dev.check(tag.devFeature()); diff --git a/src/link/SpirV.zig b/src/link/SpirV.zig index 153b15609a06af4bec83f886fda413a20c0035b0..5e0f31682e89d3266e32c924da9b7023b01c0462 100644 --- a/src/link/SpirV.zig +++ b/src/link/SpirV.zig @@ -10,21 +10,33 @@ const Compilation = @import("../Compilation.zig"); const link = @import("../link.zig"); const Air = @import("../Air.zig"); const Type = @import("../Type.zig"); +const codegen = @import("../codegen.zig"); const CodeGen = @import("../codegen/spirv/CodeGen.zig"); const Module = @import("../codegen/spirv/Module.zig"); const trace = @import("../tracy.zig").trace; const BinaryModule = @import("SpirV/BinaryModule.zig"); const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig"); +const dedup_types = @import("SpirV/dedup_types.zig"); +const prune_unused = @import("SpirV/prune_unused.zig"); const spec = @import("../codegen/spirv/spec.zig"); +const Section = @import("../codegen/spirv/Section.zig"); const Id = spec.Id; const Word = spec.Word; +const Mir = @import("../codegen/spirv/Mir.zig"); const Linker = @This(); base: link.File, -module: Module, -cg: CodeGen, +fragments: std.AutoArrayHashMapUnmanaged(InternPool.Nav.Index, Mir) = .empty, +pending_navs: std.ArrayListUnmanaged(InternPool.Nav.Index) = .empty, +entry_points: std.ArrayListUnmanaged(EntryPointDecl) = .empty, + +const EntryPointDecl = struct { + nav: InternPool.Nav.Index, + name: []const u8, + cc: std.builtin.CallingConvention, +}; pub fn createEmpty( arena: Allocator, @@ -32,7 +44,6 @@ pub fn createEmpty( emit: Path, options: link.File.OpenOptions, ) !*Linker { - const gpa = comp.gpa; const io = comp.io; const target = &comp.root_mod.resolved_target.result; @@ -61,21 +72,6 @@ pub fn createEmpty( .file = null, .build_id = options.build_id, }, - .module = .{ - .gpa = gpa, - .arena = arena, - .zcu = comp.zcu.?, - }, - .cg = .{ - // These fields are populated in generate() - .pt = undefined, - .air = undefined, - .liveness = undefined, - .owner_nav = undefined, - .module = undefined, - .control_flow = .{ .structured = .{} }, - .base_line = undefined, - }, }; errdefer linker.deinit(); @@ -97,70 +93,55 @@ pub fn open( } pub fn deinit(linker: *Linker) void { - linker.cg.deinit(); - linker.module.deinit(); -} - -fn generate( - linker: *Linker, - pt: Zcu.PerThread, - nav_index: InternPool.Nav.Index, - air: Air, - liveness: Air.Liveness, - do_codegen: bool, -) !void { - const zcu = pt.zcu; - const gpa = zcu.gpa; - const structured_cfg = zcu.navFileScope(nav_index).mod.?.structured_cfg; - - linker.cg.control_flow.deinit(gpa); - linker.cg.args.clearRetainingCapacity(); - linker.cg.inst_results.clearRetainingCapacity(); - linker.cg.id_scratch.clearRetainingCapacity(); - linker.cg.prologue.reset(); - linker.cg.body.reset(); - - linker.cg = .{ - .pt = pt, - .air = air, - .liveness = liveness, - .owner_nav = nav_index, - .module = &linker.module, - .control_flow = switch (structured_cfg) { - true => .{ .structured = .{} }, - false => .{ .unstructured = .{} }, - }, - .base_line = zcu.navSrcLine(nav_index), - - .args = linker.cg.args, - .inst_results = linker.cg.inst_results, - .id_scratch = linker.cg.id_scratch, - .prologue = linker.cg.prologue, - .body = linker.cg.body, - }; - - linker.cg.genNav(do_codegen) catch |err| switch (err) { - error.AlreadyReported => return, - else => |e| return e, - }; + const gpa = linker.base.comp.gpa; + for (linker.fragments.values()) |*mir| { + mir.deinit(gpa); + } + linker.fragments.deinit(gpa); + linker.pending_navs.deinit(gpa); + linker.entry_points.deinit(gpa); } pub fn updateFunc( linker: *Linker, pt: Zcu.PerThread, func_index: InternPool.Index, - air: *const Air, - liveness: *const ?Air.Liveness, + mir: *codegen.AnyMir, ) !void { + const gpa = linker.base.comp.gpa; const nav = pt.zcu.funcInfo(func_index).owner_nav; - // TODO: Separate types for generating decls and functions? - try linker.generate(pt, nav, air.*, liveness.*.?, true); + + if (linker.fragments.getPtr(nav)) |existing| { + existing.deinit(gpa); + } + + try linker.fragments.put(gpa, nav, mir.spirv); + mir.spirv = .{ + .extended_instruction_set = &.{}, + .globals = &.{}, + .functions = &.{}, + .annotations = &.{}, + .debug_names = &.{}, + .debug_strings = &.{}, + .execution_modes = &.{}, + .id_bound = 0, + .owner_nav = mir.spirv.owner_nav, + .kind = mir.spirv.kind, + .decl_result_id = .none, + .nav_refs = &.{}, + .uav_refs = &.{}, + .decl_deps = &.{}, + .internal_globals = &.{}, + .entry_points = &.{}, + }; } pub fn updateNav(linker: *Linker, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.Error!void { const ip = &pt.zcu.intern_pool; - log.debug("lowering nav {f}({d})", .{ ip.getNav(nav).fqn.fmt(ip), nav }); - try linker.generate(pt, nav, undefined, undefined, false); + log.debug("deferring nav {f}({d}) to flush", .{ ip.getNav(nav).fqn.fmt(ip), nav }); + + const gpa = linker.base.comp.gpa; + linker.pending_navs.append(gpa, nav) catch return error.OutOfMemory; } pub fn updateExports( @@ -171,6 +152,7 @@ pub fn updateExports( ) !void { const zcu = pt.zcu; const ip = &zcu.intern_pool; + const gpa = linker.base.comp.gpa; const nav_index = switch (exported) { .nav => |nav| nav, .uav => |uav| { @@ -180,21 +162,18 @@ pub fn updateExports( }; const nav_ty = ip.getNav(nav_index).resolved.?.type; if (ip.isFunctionType(nav_ty)) { - const spv_decl_index = try linker.module.resolveNav(ip, nav_index); const cc = Type.fromInterned(nav_ty).fnCallingConvention(zcu); if (cc == .spirv_device) return; for (export_indices) |export_idx| { const exp = export_idx.ptr(zcu); - try linker.module.declareEntryPoint( - spv_decl_index, - exp.opts.name.toSlice(ip), - cc, - ); + try linker.entry_points.append(gpa, .{ + .nav = nav_index, + .name = exp.opts.name.toSlice(ip), + .cc = cc, + }); } } - - // TODO: Export regular functions, variables, etc using Linkage attributes. } pub fn flush( @@ -203,11 +182,6 @@ pub fn flush( tid: Zcu.PerThread.Id, prog_node: std.Progress.Node, ) link.Error!void { - // The goal is to never use this because it's only needed if we need to - // write to InternPool, but flush is too late to be writing to the - // InternPool. - _ = tid; - const tracy = trace(@src()); defer tracy.end(); @@ -219,19 +193,334 @@ pub fn flush( const gpa = comp.gpa; const io = comp.io; - // 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 error_info: std.Io.Writer.Allocating = .init(linker.module.gpa); + const zcu = comp.zcu.?; + const active = zcu.activate(tid); + defer active.deactivate(); + const pt = active.pt; + for (linker.pending_navs.items) |nav| { + if (linker.fragments.contains(nav)) continue; + + const mir = CodeGen.generateNav(pt, nav) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + error.AlreadyReported => continue, + error.Canceled => return error.Canceled, + }; + + linker.fragments.put(gpa, nav, mir) catch return error.OutOfMemory; + } + linker.pending_navs.clearRetainingCapacity(); + + const merged = mergeFragments(linker, gpa, arena) catch |err| switch (err) { + error.OutOfMemory => return error.OutOfMemory, + }; + + var binary = linkModule(arena, merged.words, merged.id_bound, sub_prog_node) catch |err| switch (err) { + error.OutOfMemory => |e| return e, + else => |other| return diags.fail("error while linking: {s}", .{@errorName(other)}), + }; + defer binary.deinit(arena); + + const header = [_]Word{ + spec.magic_number, + merged.version.toWord(), + merged.generator_id, + binary.id_bound, + 0, + }; + + linker.base.file.?.writeStreamingAll(io, @ptrCast(&header)) catch |err| + return diags.fail("failed to write: {t}", .{err}); + linker.base.file.?.writeStreamingAll(io, @ptrCast(binary.instructions)) catch |err| + return diags.fail("failed to write: {t}", .{err}); +} + +fn linkModule(arena: Allocator, words: []const Word, id_bound: u32, progress: std.Progress.Node) !BinaryModule { + var parser = try BinaryModule.Parser.init(arena); + defer parser.deinit(); + var binary = try parser.initFromWords(words, id_bound); + try prune_unused.run(&parser, &binary); + try dedup_types.run(&parser, &binary); + try lower_invocation_globals.run(&parser, &binary, progress); + return binary; +} + +fn mergeFragments(linker: *Linker, gpa: Allocator, arena: Allocator) error{OutOfMemory}!MergedModule { + const comp = linker.base.comp; + const zcu = comp.zcu.?; + const target = zcu.getTarget(); + + var next_id: Word = 1; + + var nav_final_ids: std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id) = .empty; + defer nav_final_ids.deinit(gpa); + + var uav_final_ids: std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id) = .empty; + defer uav_final_ids.deinit(gpa); + + var frag_infos: std.ArrayList(FragmentInfo) = .empty; + defer frag_infos.deinit(gpa); + try frag_infos.ensureTotalCapacity(gpa, @intCast(linker.fragments.count())); + + for (linker.fragments.keys(), linker.fragments.values()) |nav, *mir| { + const id_offset = next_id - 1; + frag_infos.appendAssumeCapacity(.{ + .id_offset = id_offset, + }); + + if (mir.decl_result_id != .none) { + try nav_final_ids.put(gpa, nav, @enumFromInt(@intFromEnum(mir.decl_result_id) + id_offset)); + } + + next_id += mir.id_bound - 1; + } + + for (linker.fragments.values(), frag_infos.items) |*mir, frag_info| { + for (mir.nav_refs) |ref| { + if (!nav_final_ids.contains(ref.nav)) { + try nav_final_ids.put(gpa, ref.nav, @enumFromInt(@intFromEnum(ref.local_id) + frag_info.id_offset)); + } + } + + for (mir.uav_refs) |ref| { + const key = .{ ref.val, ref.storage_class }; + if (!uav_final_ids.contains(key)) { + try uav_final_ids.put(gpa, key, @enumFromInt(@intFromEnum(ref.local_id) + frag_info.id_offset)); + } + } + } + + var parser = BinaryModule.Parser.init(gpa) catch return error.OutOfMemory; + defer parser.deinit(); + var ext_inst_section = Section{}; + defer ext_inst_section.deinit(gpa); + var globals_section = Section{}; + defer globals_section.deinit(gpa); + var functions_section = Section{}; + defer functions_section.deinit(gpa); + var annotations_section = Section{}; + defer annotations_section.deinit(gpa); + var debug_names_section = Section{}; + defer debug_names_section.deinit(gpa); + var debug_strings_section = Section{}; + defer debug_strings_section.deinit(gpa); + var execution_modes_section = Section{}; + defer execution_modes_section.deinit(gpa); + + for (linker.fragments.values(), frag_infos.items) |*mir, frag_info| { + var id_remap: std.AutoHashMapUnmanaged(Id, Id) = .empty; + defer id_remap.deinit(gpa); + + for (mir.nav_refs) |ref| { + if (nav_final_ids.get(ref.nav)) |final_id| { + try id_remap.put(gpa, ref.local_id, final_id); + } + } + + for (mir.uav_refs) |ref| { + const key = .{ ref.val, ref.storage_class }; + if (uav_final_ids.get(key)) |final_id| { + try id_remap.put(gpa, ref.local_id, final_id); + } + } + + try remapAndAppend(gpa, &ext_inst_section, mir.extended_instruction_set, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &globals_section, mir.globals, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &functions_section, mir.functions, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &annotations_section, mir.annotations, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &debug_names_section, mir.debug_names, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &debug_strings_section, mir.debug_strings, frag_info.id_offset, &id_remap, &parser); + try remapAndAppend(gpa, &execution_modes_section, mir.execution_modes, frag_info.id_offset, &id_remap, &parser); + + for (mir.entry_points) |ep| { + try linker.entry_points.append(gpa, .{ + .nav = mir.owner_nav, + .name = ep.name, + .cc = ep.cc, + }); + } + } + + var capabilities_section = Section{}; + defer capabilities_section.deinit(gpa); + var extensions_section = Section{}; + defer extensions_section.deinit(gpa); + var memory_model_section = Section{}; + defer memory_model_section.deinit(gpa); + var entry_points_section = Section{}; + defer entry_points_section.deinit(gpa); + + const cap_pairs = [_]struct { cap: spec.Capability, ext: ?[]const u8 }{ + .{ .cap = .int8, .ext = null }, + .{ .cap = .int16, .ext = null }, + }; + for (cap_pairs) |pair| { + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = pair.cap }); + if (pair.ext) |ext| { + try extensions_section.emit(gpa, .OpExtension, .{ .name = ext }); + } + } + + switch (target.os.tag) { + .opengl => { + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .shader }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .matrix }); + }, + .vulkan => { + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .shader }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .matrix }); + if (target.cpu.arch == .spirv64) { + try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_physical_storage_buffer" }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .physical_storage_buffer_addresses }); + } + }, + .opencl, .amdhsa => { + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .kernel }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .addresses }); + }, + else => unreachable, + } + if (target.cpu.arch == .spirv64) + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .int64 }); + if (target.cpu.has(.spirv, .int64)) + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .int64 }); + if (target.cpu.has(.spirv, .float16)) { + if (target.os.tag == .opencl) try extensions_section.emit(gpa, .OpExtension, .{ .name = "cl_khr_fp16" }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .float16 }); + } + if (target.cpu.has(.spirv, .float64)) + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .float64 }); + if (target.cpu.has(.spirv, .generic_pointer)) + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .generic_pointer }); + if (target.cpu.has(.spirv, .vector16)) + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .vector16 }); + if (target.cpu.has(.spirv, .storage_push_constant16)) { + try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_16bit_storage" }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .storage_push_constant16 }); + } + if (target.cpu.has(.spirv, .arbitrary_precision_integers)) { + try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_INTEL_arbitrary_precision_integers" }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .arbitrary_precision_integers_intel }); + } + if (target.cpu.has(.spirv, .variable_pointers)) { + try extensions_section.emit(gpa, .OpExtension, .{ .name = "SPV_KHR_variable_pointers" }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .variable_pointers_storage_buffer }); + try capabilities_section.emit(gpa, .OpCapability, .{ .capability = .variable_pointers }); + } + + const addressing_model: spec.AddressingModel = switch (target.os.tag) { + .opengl => .logical, + .vulkan => if (target.cpu.arch == .spirv32) .logical else .physical_storage_buffer64, + .opencl => if (target.cpu.arch == .spirv32) .physical32 else .physical64, + .amdhsa => .physical64, + else => unreachable, + }; + try memory_model_section.emit(gpa, .OpMemoryModel, .{ + .addressing_model = addressing_model, + .memory_model = switch (target.os.tag) { + .opencl => .open_cl, + .vulkan, .opengl => .glsl450, + else => unreachable, + }, + }); + + for (linker.entry_points.items) |ep| { + const final_id = nav_final_ids.get(ep.nav) orelse continue; + + var interface: std.ArrayList(Id) = .empty; + defer interface.deinit(gpa); + + var visited: std.AutoHashMapUnmanaged(InternPool.Nav.Index, void) = .empty; + defer visited.deinit(gpa); + + try collectEntryPointInterface(linker, ep.nav, &interface, &visited, &nav_final_ids, &uav_final_ids, &frag_infos, gpa); + + const exec_model: spec.ExecutionModel = switch (target.os.tag) { + .vulkan, .opengl => switch (ep.cc) { + .spirv_vertex => .vertex, + .spirv_fragment => .fragment, + .spirv_kernel => .gl_compute, + .spirv_task => .task_ext, + .spirv_mesh => .mesh_ext, + .spirv_device => continue, + else => unreachable, + }, + .opencl => switch (ep.cc) { + .spirv_kernel => .kernel, + .spirv_device => continue, + else => unreachable, + }, + else => unreachable, + }; + + try entry_points_section.emit(gpa, .OpEntryPoint, .{ + .execution_model = exec_model, + .entry_point = final_id, + .name = ep.name, + .interface = interface.items, + }); + + switch (ep.cc) { + .spirv_kernel, .spirv_task => |kernel| { + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = .{ .local_size = .{ + .x_size = kernel.x, + .y_size = kernel.y, + .z_size = kernel.z, + } }, + }); + }, + .spirv_fragment => |fragment| { + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = if (target.os.tag == .vulkan) .origin_upper_left else .origin_lower_left, + }); + if (fragment.pixel_centered_integer) { + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = .pixel_center_integer, + }); + } + const exec_mode: ?spec.ExecutionMode.Extended = switch (fragment.depth_assumption) { + .none => null, + .greater => .depth_greater, + .less => .depth_less, + .unchanged => .depth_unchanged, + }; + if (exec_mode) |mode| { + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = mode, + }); + } + }, + .spirv_mesh => |mesh| { + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = .{ .output_vertices = .{ .vertex_count = mesh.max_vertices } }, + }); + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = .{ .output_primitives_ext = .{ .primitive_count = mesh.max_primitives } }, + }); + try execution_modes_section.emit(gpa, .OpExecutionMode, .{ + .entry_point = final_id, + .mode = switch (mesh.stage_output) { + .output_points => .output_points, + .output_lines => .output_lines_ext, + .output_triangles => .output_triangles_ext, + }, + }); + }, + else => {}, + } + } + + const ip = &zcu.intern_pool; + var error_info: std.Io.Writer.Allocating = .init(gpa); defer error_info.deinit(); - error_info.writer.writeAll("zig_errors:") catch return error.OutOfMemory; - const ip = &linker.base.comp.zcu.?.intern_pool; for (ip.global_error_set.getNamesFromMainThread()) |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. error_info.writer.writeByte(':') catch return error.OutOfMemory; std.Uri.Component.percentEncode( &error_info.writer, @@ -246,27 +535,249 @@ pub fn flush( }.isValidChar, ) catch return error.OutOfMemory; } - try linker.module.sections.debug_strings.emit(gpa, .OpSourceExtension, .{ + try debug_strings_section.emit(gpa, .OpSourceExtension, .{ .extension = error_info.written(), }); - const module = try linker.module.finalize(arena); - errdefer arena.free(module); + const zig_version = @import("builtin").zig_version; + const zig_spirv_compiler_version = comptime (zig_version.major << 12) | (zig_version.minor << 7) | zig_version.patch; + try debug_strings_section.emit(gpa, .OpSource, .{ + .source_language = .zig, + .version = zig_spirv_compiler_version, + .file = null, + .source = null, + }); - const linked_module = linkModule(arena, module, sub_prog_node) catch |err| switch (err) { - error.OutOfMemory => |e| return e, - else => |other| return diags.fail("error while linking: {s}", .{@errorName(other)}), + const version: spec.Version = .{ + .major = 1, + .minor = blk: { + if (target.cpu.has(.spirv, .v1_6)) break :blk 6; + if (target.cpu.has(.spirv, .v1_5)) break :blk 5; + if (target.cpu.has(.spirv, .v1_4)) break :blk 4; + if (target.cpu.has(.spirv, .v1_3)) break :blk 3; + if (target.cpu.has(.spirv, .v1_2)) break :blk 2; + if (target.cpu.has(.spirv, .v1_1)) break :blk 1; + break :blk 0; + }, }; - // TODO endianness bug. use file writer and call writeSliceEndian instead - linker.base.file.?.writeStreamingAll(io, @ptrCast(linked_module)) catch |err| - return diags.fail("failed to write: {t}", .{err}); + const generator_id: u32 = (spec.zig_generator_id << 16) | zig_spirv_compiler_version; + + const buffers = &[_][]const Word{ + capabilities_section.toWords(), + extensions_section.toWords(), + ext_inst_section.toWords(), + memory_model_section.toWords(), + entry_points_section.toWords(), + execution_modes_section.toWords(), + debug_strings_section.toWords(), + debug_names_section.toWords(), + annotations_section.toWords(), + globals_section.toWords(), + functions_section.toWords(), + }; + + var total_size: usize = 0; + for (buffers) |buffer| { + total_size += buffer.len; + } + const result = try arena.alloc(Word, total_size); + + var offset: usize = 0; + for (buffers) |buffer| { + @memcpy(result[offset..][0..buffer.len], buffer); + offset += buffer.len; + } + + return .{ + .words = result, + .id_bound = next_id, + .version = version, + .generator_id = generator_id, + }; +} + +const MergedModule = struct { + words: []const Word, + id_bound: Word, + version: spec.Version, + generator_id: u32, +}; + +const FragmentInfo = struct { + id_offset: Word, +}; + +fn collectEntryPointInterface( + linker: *Linker, + nav: InternPool.Nav.Index, + interface: *std.ArrayList(Id), + visited: *std.AutoHashMapUnmanaged(InternPool.Nav.Index, void), + nav_final_ids: *const std.AutoHashMapUnmanaged(InternPool.Nav.Index, Id), + uav_final_ids: *const std.AutoHashMapUnmanaged(struct { InternPool.Index, spec.StorageClass }, Id), + frag_infos: *const std.ArrayList(FragmentInfo), + gpa: Allocator, +) error{OutOfMemory}!void { + const visited_gop = try visited.getOrPut(gpa, nav); + if (visited_gop.found_existing) return; + + const frag_index = linker.fragments.getIndex(nav) orelse return; + const mir = &linker.fragments.values()[frag_index]; + const id_offset = frag_infos.items[frag_index].id_offset; + + if (mir.kind == .global) { + if (nav_final_ids.get(nav)) |final_id| { + try interface.append(gpa, final_id); + } + } + + for (mir.uav_refs) |ref| { + if (ref.kind == .global) { + if (uav_final_ids.get(.{ ref.val, ref.storage_class })) |final_id| { + try interface.append(gpa, final_id); + } + } + } + + for (mir.internal_globals) |local_id| { + const global_id: Id = @enumFromInt(@intFromEnum(local_id) + id_offset); + try interface.append(gpa, global_id); + } + + for (mir.decl_deps) |dep| { + try collectEntryPointInterface(linker, dep.nav, interface, visited, nav_final_ids, uav_final_ids, frag_infos, gpa); + } + + for (mir.nav_refs) |ref| { + try collectEntryPointInterface(linker, ref.nav, interface, visited, nav_final_ids, uav_final_ids, frag_infos, gpa); + } +} + +fn remapAndAppend( + gpa: Allocator, + dest: *Section, + words: []const Word, + id_offset: Word, + id_remap: *const std.AutoHashMapUnmanaged(Id, Id), + parser: *BinaryModule.Parser, +) error{OutOfMemory}!void { + if (words.len == 0) return; + + try dest.instructions.ensureUnusedCapacity(gpa, words.len); + + var iter = BinaryModule.Instruction.Iterator.init(words, 0); + while (iter.next()) |inst| { + const dest_start = dest.instructions.items.len; + const inst_words = words[inst.offset..][0..((words[inst.offset] >> 16))]; + dest.instructions.appendSliceAssumeCapacity(inst_words); + const inst_slice = dest.instructions.items[dest_start..][0..inst_words.len]; + + const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; + var offset: usize = 0; + for (inst_spec.operands) |operand| { + const cat = operand.kind.category(); + switch (operand.quantifier) { + .required => { + if (offset >= inst.operands.len) break; + if (cat == .id) { + remapSingleId(&inst_slice[1 + offset], id_offset, id_remap); + offset += 1; + } else if (cat == .literal) { + offset += operandLiteralWordCount(operand.kind, inst, offset); + } else if (cat == .composite) { + remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap); + offset += 2; + } else { + offset += 1; + } + }, + .optional => { + if (offset >= inst.operands.len) break; + if (cat == .id) { + remapSingleId(&inst_slice[1 + offset], id_offset, id_remap); + offset += 1; + } else if (cat == .literal) { + offset += operandLiteralWordCount(operand.kind, inst, offset); + } else { + offset += 1; + } + }, + .variadic => { + while (offset < inst.operands.len) { + if (cat == .id) { + remapSingleId(&inst_slice[1 + offset], id_offset, id_remap); + offset += 1; + } else if (cat == .literal) { + offset += operandLiteralWordCount(operand.kind, inst, offset); + } else if (cat == .composite) { + if (offset + 1 < inst.operands.len) { + remapCompositeOperand(operand.kind, inst_slice, offset, id_offset, id_remap); + } + offset += 2; + } else { + offset += 1; + } + } + }, + } + } + } +} + +fn remapCompositeOperand( + kind: spec.OperandKind, + inst_slice: []Word, + offset: usize, + id_offset: Word, + id_remap: *const std.AutoHashMapUnmanaged(Id, Id), +) void { + switch (kind) { + .pair_literal_integer_id_ref => { + remapSingleId(&inst_slice[1 + offset + 1], id_offset, id_remap); + }, + .pair_id_ref_literal_integer => { + remapSingleId(&inst_slice[1 + offset], id_offset, id_remap); + }, + .pair_id_ref_id_ref => { + remapSingleId(&inst_slice[1 + offset], id_offset, id_remap); + remapSingleId(&inst_slice[1 + offset + 1], id_offset, id_remap); + }, + else => {}, + } +} + +fn operandLiteralWordCount(kind: spec.OperandKind, inst: BinaryModule.Instruction, offset: usize) usize { + return switch (kind) { + .literal_integer, .literal_float => 1, + .literal_string => blk: { + var count: usize = 0; + var off = offset; + while (off < inst.operands.len) { + const word = inst.operands[off]; + count += 1; + off += 1; + if (word & 0xFF000000 == 0 or + word & 0x00FF0000 == 0 or + word & 0x0000FF00 == 0 or + word & 0x000000FF == 0) + { + break; + } + } + break :blk count; + }, + .literal_context_dependent_number => inst.operands.len - offset, + .literal_ext_inst_integer => 1, + else => 1, + }; } -fn linkModule(arena: Allocator, module: []Word, progress: std.Progress.Node) ![]Word { - var parser = try BinaryModule.Parser.init(arena); - defer parser.deinit(); - var binary = try parser.parse(module); - try lower_invocation_globals.run(&parser, &binary, progress); - return binary.finalize(arena); +fn remapSingleId(word: *Word, id_offset: Word, id_remap: *const std.AutoHashMapUnmanaged(Id, Id)) void { + const id: Id = @enumFromInt(word.*); + if (id == .none) return; + if (id_remap.get(id)) |final_id| { + word.* = @intFromEnum(final_id); + } else { + word.* = @intFromEnum(id) + id_offset; + } } diff --git a/src/link/SpirV/BinaryModule.zig b/src/link/SpirV/BinaryModule.zig index e639994f33b7df39e90814768983803cbb898d17..55604fc194d6e70df9fbd54eb417b6e55426eea5 100644 --- a/src/link/SpirV/BinaryModule.zig +++ b/src/link/SpirV/BinaryModule.zig @@ -11,278 +11,161 @@ const ResultId = spec.Id; const BinaryModule = @This(); -pub const header_words = 5; - -/// The module SPIR-V version. -version: spec.Version, - -/// The generator magic number. -generator_magic: u32, - /// The result-id bound of this SPIR-V module. id_bound: u32, -/// The instructions of this module. This does not contain the header. +/// The instructions of this module (no header). instructions: []const Word, /// Maps OpExtInstImport result-ids to their InstructionSet. ext_inst_map: std.AutoHashMapUnmanaged(ResultId, InstructionSet), -/// This map contains the width of arithmetic types (OpTypeInt and -/// OpTypeFloat). We need this information to correctly parse the operands -/// of Op(Spec)Constant and OpSwitch. +/// Width of arithmetic types (OpTypeInt/OpTypeFloat). Needed to correctly +/// parse operands of Op(Spec)Constant and OpSwitch. arith_type_width: std.AutoHashMapUnmanaged(ResultId, u16), -/// The starting offsets of some sections -sections: struct { - functions: usize, -}, +functions_start: usize, -pub fn deinit(self: *BinaryModule, a: Allocator) void { - self.ext_inst_map.deinit(a); - self.arith_type_width.deinit(a); - self.* = undefined; +pub fn deinit(bm: *BinaryModule, gpa: Allocator) void { + bm.ext_inst_map.deinit(gpa); + bm.arith_type_width.deinit(gpa); + bm.* = undefined; } -pub fn iterateInstructions(self: BinaryModule) Instruction.Iterator { - return Instruction.Iterator.init(self.instructions, 0); +pub fn iterateInstructions(bm: BinaryModule) Instruction.Iterator { + return Instruction.Iterator.init(bm.instructions, 0); } -pub fn iterateInstructionsFrom(self: BinaryModule, offset: usize) Instruction.Iterator { - return Instruction.Iterator.init(self.instructions, offset); +pub fn iterateInstructionsFrom(bm: BinaryModule, offset: usize) Instruction.Iterator { + return Instruction.Iterator.init(bm.instructions, offset); } -pub fn instructionAt(self: BinaryModule, offset: usize) Instruction { - var it = self.iterateInstructionsFrom(offset); - return it.next().?; -} - -pub fn finalize(self: BinaryModule, a: Allocator) ![]Word { - const result = try a.alloc(Word, 5 + self.instructions.len); - errdefer a.free(result); - - result[0] = spec.magic_number; - result[1] = @bitCast(self.version); - result[2] = @bitCast(self.generator_magic); - result[3] = self.id_bound; - result[4] = 0; // Schema - - @memcpy(result[5..], self.instructions); - return result; -} - -/// Errors that can be raised when the module is not correct. -/// Note that the parser doesn't validate SPIR-V modules by a -/// long shot. It only yields errors that critically prevent -/// further analysis of the module. -pub const ParseError = error{ - /// Raised when the module doesn't start with the SPIR-V magic. - /// This usually means that the module isn't actually SPIR-V. - InvalidMagic, - /// Raised when the module has an invalid "physical" format: - /// For example when the header is incomplete, or an instruction - /// has an illegal format. - InvalidPhysicalFormat, - /// OpExtInstImport was used with an unknown extension string. - InvalidExtInstImport, - /// The module had an instruction with an invalid (unknown) opcode. - InvalidOpcode, - /// An instruction's operands did not conform to the SPIR-V specification - /// for that instruction. - InvalidOperands, - /// A result-id was declared more than once. - DuplicateId, - /// Some ID did not resolve. - InvalidId, - /// This opcode or instruction is not supported yet. - UnsupportedOperation, - /// Parser ran out of memory. - OutOfMemory, -}; - pub const Instruction = struct { pub const Iterator = struct { words: []const Word, - index: usize = 0, offset: usize = 0, pub fn init(words: []const Word, start_offset: usize) Iterator { return .{ .words = words, .offset = start_offset }; } - pub fn next(self: *Iterator) ?Instruction { - if (self.offset >= self.words.len) return null; + pub fn next(it: *Iterator) ?Instruction { + if (it.offset >= it.words.len) return null; - const instruction_len = self.words[self.offset] >> 16; - defer self.offset += instruction_len; - defer self.index += 1; + const instruction_len = it.words[it.offset] >> 16; + defer it.offset += instruction_len; assert(instruction_len != 0); - assert(self.offset < self.words.len); + assert(it.offset < it.words.len); return Instruction{ - .opcode = @enumFromInt(self.words[self.offset] & 0xFFFF), - .index = self.index, - .offset = self.offset, - .operands = self.words[self.offset..][1..instruction_len], + .opcode = @enumFromInt(it.words[it.offset] & 0xFFFF), + .offset = it.offset, + .operands = it.words[it.offset..][1..instruction_len], }; } }; - /// The opcode for this instruction. opcode: Opcode, - /// The instruction's index. - index: usize, - /// The instruction's word offset in the module. offset: usize, - /// The raw (unparsed) operands for this instruction. operands: []const Word, }; -/// This parser contains information (acceleration tables) -/// that can be persisted across different modules. This is -/// used to initialize the module, and is also used when -/// further analyzing it. pub const Parser = struct { - /// The allocator used to allocate this parser's structures, - /// and also the structures of any parsed module. - a: Allocator, - - /// Maps (instruction set, opcode) => instruction index (for instruction set) + gpa: Allocator, opcode_table: std.AutoHashMapUnmanaged(u32, u16) = .empty, - pub fn init(a: Allocator) !Parser { - var self = Parser{ - .a = a, - }; - errdefer self.deinit(); + pub fn init(gpa: Allocator) !Parser { + var parser = Parser{ .gpa = gpa }; + errdefer parser.deinit(); inline for (std.meta.tags(InstructionSet)) |set| { const instructions = set.instructions(); - try self.opcode_table.ensureUnusedCapacity(a, @intCast(instructions.len)); + try parser.opcode_table.ensureUnusedCapacity(gpa, @intCast(instructions.len)); for (instructions, 0..) |inst, i| { - // Note: Some instructions may alias another. In this case we don't really care - // which one is first: they all (should) have the same operands anyway. Just pick - // the first, which is usually the core, KHR or EXT variant. - const entry = self.opcode_table.getOrPutAssumeCapacity(mapSetAndOpcode(set, @intCast(inst.opcode))); + const entry = parser.opcode_table.getOrPutAssumeCapacity(mapSetAndOpcode(set, @intCast(inst.opcode))); if (!entry.found_existing) { entry.value_ptr.* = @intCast(i); } } } - return self; + return parser; } - pub fn deinit(self: *Parser) void { - self.opcode_table.deinit(self.a); + pub fn deinit(parser: *Parser) void { + parser.opcode_table.deinit(parser.gpa); } fn mapSetAndOpcode(set: InstructionSet, opcode: u16) u32 { return (@as(u32, @intFromEnum(set)) << 16) | opcode; } - pub fn getInstSpec(self: Parser, opcode: Opcode) ?spec.Instruction { - const index = self.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(opcode))) orelse return null; + pub fn getInstSpec(parser: Parser, opcode: Opcode) ?spec.Instruction { + const index = parser.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(opcode))) orelse return null; return InstructionSet.core.instructions()[index]; } - pub fn parse(self: *Parser, module: []const u32) ParseError!BinaryModule { - if (module[0] != spec.magic_number) { - return error.InvalidMagic; - } else if (module.len < header_words) { - log.err("module only has {}/{} header words", .{ module.len, header_words }); - return error.InvalidPhysicalFormat; - } - + /// Build a BinaryModule from raw instruction words (no header). + /// Scans for ext_inst_map, arith_type_width, and the functions section offset. + pub fn initFromWords(parser: *Parser, words: []const Word, id_bound: u32) !BinaryModule { var binary = BinaryModule{ - .version = @bitCast(module[1]), - .generator_magic = @bitCast(module[2]), - .id_bound = module[3], - .instructions = module[header_words..], + .id_bound = id_bound, + .instructions = words, .ext_inst_map = .{}, .arith_type_width = .{}, - .sections = undefined, + .functions_start = undefined, }; var maybe_function_section: ?usize = null; + var it = binary.iterateInstructions(); + while (it.next()) |inst| { + const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; + const operands = inst.operands; - // First pass through the module to verify basic structure and - // to gather some initial stuff for more detailed analysis. - // We want to check some stuff that Instruction.Iterator is no good for, - // so just iterate manually. - var offset: usize = 0; - while (offset < binary.instructions.len) { - const len = binary.instructions[offset] >> 16; - if (len == 0 or len + offset > binary.instructions.len) { - log.err("invalid instruction format: len={}, end={}, module len={}", .{ len, len + offset, binary.instructions.len }); - return error.InvalidPhysicalFormat; - } - defer offset += len; - - // We can't really efficiently use non-exhaustive enums here, because we would - // need to manually write out all valid cases. Since we have this map anyway, just - // use that. - const opcode: Opcode = @enumFromInt(@as(u16, @truncate(binary.instructions[offset]))); - const inst_spec = self.getInstSpec(opcode) orelse { - log.err("invalid opcode for core set: {}", .{@intFromEnum(opcode)}); - return error.InvalidOpcode; - }; - - const operands = binary.instructions[offset..][1..len]; - switch (opcode) { + switch (inst.opcode) { .OpExtInstImport => { const set_name = std.mem.sliceTo(std.mem.sliceAsBytes(operands[1..]), 0); - const set = std.meta.stringToEnum(InstructionSet, set_name) orelse { - log.err("invalid instruction set '{s}'", .{set_name}); - return error.InvalidExtInstImport; - }; - if (set == .core) return error.InvalidExtInstImport; - try binary.ext_inst_map.put(self.a, @enumFromInt(operands[0]), set); + const set = std.meta.stringToEnum(InstructionSet, set_name) orelse continue; + if (set == .core) continue; + try binary.ext_inst_map.put(parser.gpa, @enumFromInt(operands[0]), set); }, .OpTypeInt, .OpTypeFloat => { - const entry = try binary.arith_type_width.getOrPut(self.a, @enumFromInt(operands[0])); - if (entry.found_existing) return error.DuplicateId; - entry.value_ptr.* = std.math.cast(u16, operands[1]) orelse return error.InvalidOperands; + try binary.arith_type_width.put(parser.gpa, @enumFromInt(operands[0]), @intCast(operands[1])); }, .OpFunction => if (maybe_function_section == null) { - maybe_function_section = offset; + maybe_function_section = inst.offset; }, else => {}, } - // OpSwitch takes a value as argument, not an OpType... hence we need to populate arith_type_width - // with ALL operations that return an int or float. + // propagate arith type widths through instructions that return int/float const spec_operands = inst_spec.operands; if (spec_operands.len >= 2 and spec_operands[0].kind == .id_result_type and spec_operands[1].kind == .id_result) { - if (operands.len < 2) return error.InvalidOperands; - if (binary.arith_type_width.get(@enumFromInt(operands[0]))) |width| { - const entry = try binary.arith_type_width.getOrPut(self.a, @enumFromInt(operands[1])); - if (entry.found_existing) return error.DuplicateId; - entry.value_ptr.* = width; + if (operands.len >= 2) { + if (binary.arith_type_width.get(@enumFromInt(operands[0]))) |width| { + try binary.arith_type_width.put(parser.gpa, @enumFromInt(operands[1]), width); + } } } } - binary.sections = .{ - .functions = maybe_function_section orelse binary.instructions.len, - }; + binary.functions_start = maybe_function_section orelse binary.instructions.len; return binary; } /// Parse offsets in the instruction that contain result-ids. /// Returned offsets are relative to inst.operands. - /// Returns in an arraylist to armortize allocations. pub fn parseInstructionResultIds( - self: *Parser, + parser: *Parser, binary: BinaryModule, inst: Instruction, - offsets: *std.array_list.Managed(u16), + offsets: *std.ArrayList(u16), ) !void { - const index = self.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(inst.opcode))).?; + const index = parser.opcode_table.get(mapSetAndOpcode(.core, @intFromEnum(inst.opcode))).?; const operands = InstructionSet.core.instructions()[index].operands; var offset: usize = 0; @@ -290,37 +173,37 @@ pub const Parser = struct { .OpSpecConstantOp => { assert(operands[0].kind == .id_result_type); assert(operands[1].kind == .id_result); - offset = try self.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); + offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); if (offset >= inst.operands.len) return error.InvalidPhysicalFormat; const spec_opcode = std.math.cast(u16, inst.operands[offset]) orelse return error.InvalidPhysicalFormat; - const spec_index = self.opcode_table.get(mapSetAndOpcode(.core, spec_opcode)) orelse + const spec_index = parser.opcode_table.get(mapSetAndOpcode(.core, spec_opcode)) orelse return error.InvalidPhysicalFormat; const spec_operands = InstructionSet.core.instructions()[spec_index].operands; assert(spec_operands[0].kind == .id_result_type); assert(spec_operands[1].kind == .id_result); - offset = try self.parseOperandsResultIds(binary, inst, spec_operands[2..], offset + 1, offsets); + offset = try parser.parseOperandsResultIds(binary, inst, spec_operands[2..], offset + 1, offsets); }, .OpExtInst => { assert(operands[0].kind == .id_result_type); assert(operands[1].kind == .id_result); - offset = try self.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); + offset = try parser.parseOperandsResultIds(binary, inst, operands[0..2], offset, offsets); if (offset + 1 >= inst.operands.len) return error.InvalidPhysicalFormat; const set_id: ResultId = @enumFromInt(inst.operands[offset]); - try offsets.append(@intCast(offset)); + try offsets.append(parser.gpa, @intCast(offset)); const set = binary.ext_inst_map.get(set_id) orelse { log.err("invalid instruction set {}", .{@intFromEnum(set_id)}); return error.InvalidId; }; const ext_opcode = std.math.cast(u16, inst.operands[offset + 1]) orelse return error.InvalidPhysicalFormat; - const ext_index = self.opcode_table.get(mapSetAndOpcode(set, ext_opcode)) orelse + const ext_index = parser.opcode_table.get(mapSetAndOpcode(set, ext_opcode)) orelse return error.InvalidPhysicalFormat; const ext_operands = set.instructions()[ext_index].operands; - offset = try self.parseOperandsResultIds(binary, inst, ext_operands, offset + 2, offsets); + offset = try parser.parseOperandsResultIds(binary, inst, ext_operands, offset + 2, offsets); }, else => { - offset = try self.parseOperandsResultIds(binary, inst, operands, offset, offsets); + offset = try parser.parseOperandsResultIds(binary, inst, operands, offset, offsets); }, } @@ -328,50 +211,50 @@ pub const Parser = struct { } fn parseOperandsResultIds( - self: *Parser, + parser: *Parser, binary: BinaryModule, inst: Instruction, operands: []const spec.Operand, start_offset: usize, - offsets: *std.array_list.Managed(u16), + offsets: *std.ArrayList(u16), ) !usize { var offset = start_offset; for (operands) |operand| { - offset = try self.parseOperandResultIds(binary, inst, operand, offset, offsets); + offset = try parser.parseOperandResultIds(binary, inst, operand, offset, offsets); } return offset; } fn parseOperandResultIds( - self: *Parser, + parser: *Parser, binary: BinaryModule, inst: Instruction, operand: spec.Operand, start_offset: usize, - offsets: *std.array_list.Managed(u16), + offsets: *std.ArrayList(u16), ) !usize { var offset = start_offset; switch (operand.quantifier) { .variadic => while (offset < inst.operands.len) { - offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); + offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); }, .optional => if (offset < inst.operands.len) { - offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); + offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); }, .required => { - offset = try self.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); + offset = try parser.parseOperandKindResultIds(binary, inst, operand.kind, offset, offsets); }, } return offset; } fn parseOperandKindResultIds( - self: *Parser, + parser: *Parser, binary: BinaryModule, inst: Instruction, kind: spec.OperandKind, start_offset: usize, - offsets: *std.array_list.Managed(u16), + offsets: *std.ArrayList(u16), ) !usize { var offset = start_offset; if (offset >= inst.operands.len) return error.InvalidPhysicalFormat; @@ -383,7 +266,7 @@ pub const Parser = struct { for (kind.enumerants()) |enumerant| { if ((mask & enumerant.value) != 0) { for (enumerant.parameters) |param_kind| { - offset = try self.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); + offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); } } } @@ -394,14 +277,14 @@ pub const Parser = struct { for (kind.enumerants()) |enumerant| { if (value == enumerant.value) { for (enumerant.parameters) |param_kind| { - offset = try self.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); + offset = try parser.parseOperandKindResultIds(binary, inst, param_kind, offset, offsets); } break; } } }, .id => { - try offsets.append(@intCast(offset)); + try offsets.append(parser.gpa, @intCast(offset)); offset += 1; }, else => switch (kind) { @@ -433,7 +316,7 @@ pub const Parser = struct { }, .literal_ext_inst_integer => unreachable, .literal_spec_constant_op_integer => unreachable, - .pair_literal_integer_id_ref => { // Switch case + .pair_literal_integer_id_ref => { assert(inst.opcode == .OpSwitch); const bit_width = binary.arith_type_width.get(@enumFromInt(inst.operands[0])) orelse { log.err("invalid OpSwitch type {}", .{inst.operands[0]}); @@ -444,16 +327,16 @@ pub const Parser = struct { 33...64 => 2, else => unreachable, }; - try offsets.append(@intCast(offset)); + try offsets.append(parser.gpa, @intCast(offset)); offset += 1; }, .pair_id_ref_literal_integer => { - try offsets.append(@intCast(offset)); + try offsets.append(parser.gpa, @intCast(offset)); offset += 2; }, .pair_id_ref_id_ref => { - try offsets.append(@intCast(offset)); - try offsets.append(@intCast(offset + 1)); + try offsets.append(parser.gpa, @intCast(offset)); + try offsets.append(parser.gpa, @intCast(offset + 1)); offset += 2; }, else => unreachable, diff --git a/src/link/SpirV/dedup_types.zig b/src/link/SpirV/dedup_types.zig new file mode 100644 index 0000000000000000000000000000000000000000..d534e10e918e8b80e5175124091098b1fcd2d372 --- /dev/null +++ b/src/link/SpirV/dedup_types.zig @@ -0,0 +1,255 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const BinaryModule = @import("BinaryModule.zig"); + +const spec = @import("../../codegen/spirv/spec.zig"); +const Word = spec.Word; +const Id = spec.Id; +const Opcode = spec.Opcode; +const Instruction = BinaryModule.Instruction; + +/// Deduplicate types and constants in a SPIR-V binary module. +/// +/// The SPIR-V spec requires that non-aggregate types be unique. +/// When merging fragments from parallel codegen, duplicate type definitions +/// may exist. This pass identifies structurally identical types/constants, +/// keeps one canonical instance, and remaps all references to duplicates. +/// +/// Decorations and names (OpName, OpMemberName) are included in the +/// equality check: two types that are structurally identical but have +/// different decorations or names are NOT considered duplicates. +pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void { + const gpa = parser.gpa; + + const Decoration = struct { offset: usize, len: usize }; + var decorations_by_id: std.array_hash_map.Auto(Id, std.ArrayList(Decoration)) = .empty; + defer { + for (decorations_by_id.values()) |*list| list.deinit(gpa); + decorations_by_id.deinit(gpa); + } + + var it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (inst.offset >= binary.functions_start) break; + switch (inst.opcode) { + .OpName, .OpMemberName => {}, + else => switch (inst.opcode.class()) { + .annotation => {}, + else => continue, + }, + } + if (inst.operands.len == 0) continue; + const target_id: Id = @enumFromInt(inst.operands[0]); + + const gop = try decorations_by_id.getOrPut(gpa, target_id); + if (!gop.found_existing) gop.value_ptr.* = .empty; + try gop.value_ptr.append(gpa, .{ + .offset = inst.offset, + .len = 1 + inst.operands.len, + }); + } + + var canonical_map: std.array_hash_map.Custom(TypeKey, Id, TypeKey.HashContext, true) = .empty; + defer { + for (canonical_map.keys()) |key| gpa.free(key.words); + canonical_map.deinit(gpa); + } + + var id_remap: std.AutoHashMapUnmanaged(Id, Id) = .empty; + defer id_remap.deinit(gpa); + + var id_offsets: std.ArrayList(u16) = .empty; + defer id_offsets.deinit(gpa); + + var key_words: std.ArrayList(Word) = .empty; + defer key_words.deinit(gpa); + + var dec_hashes: std.ArrayList(u64) = .empty; + defer dec_hashes.deinit(gpa); + + // first pass: build canonical map, identify duplicates + it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (inst.offset >= binary.functions_start) break; + if (!canDeduplicate(inst.opcode)) continue; + + const result_id_index: usize = switch (inst.opcode.class()) { + .type_declaration, .extension => 0, + .constant_creation => 1, + else => continue, + }; + if (result_id_index >= inst.operands.len) continue; + const result_id: Id = @enumFromInt(inst.operands[result_id_index]); + + key_words.items.len = 0; + try key_words.append(gpa, @intFromEnum(inst.opcode)); + + id_offsets.items.len = 0; + parser.parseInstructionResultIds(binary.*, inst, &id_offsets) catch continue; + + for (inst.operands, 0..) |word, i| { + if (i == result_id_index) continue; + if (std.mem.indexOfScalar(u16, id_offsets.items, @intCast(i)) != null) { + const canonical = id_remap.get(@enumFromInt(word)) orelse @as(Id, @enumFromInt(word)); + try key_words.append(gpa, @intFromEnum(canonical)); + } else { + try key_words.append(gpa, word); + } + } + + if (decorations_by_id.getPtr(result_id)) |dec_list| { + dec_hashes.items.len = 0; + for (dec_list.items) |dec| { + const dec_words = binary.instructions[dec.offset..][0..dec.len]; + const dec_opcode: Opcode = @enumFromInt(dec_words[0] & 0xFFFF); + var hasher = std.hash.Wyhash.init(0); + hasher.update(std.mem.asBytes(&dec_words[0])); + // OpName/OpMemberName operands are literals (member index, string), + // not ids — hash them directly without remapping + if (dec_opcode == .OpName or dec_opcode == .OpMemberName) { + hasher.update(std.mem.sliceAsBytes(dec_words[2..])); + } else { + for (dec_words[2..]) |w| { + const w_val = if (id_remap.get(@enumFromInt(w))) |c| @intFromEnum(c) else w; + hasher.update(std.mem.asBytes(&w_val)); + } + } + try dec_hashes.append(gpa, hasher.final()); + } + std.mem.sort(u64, dec_hashes.items, {}, std.sort.asc(u64)); + var prev: u64 = 0; + for (dec_hashes.items) |h| { + if (h == prev) continue; + prev = h; + try key_words.append(gpa, @truncate(h)); + try key_words.append(gpa, @truncate(h >> 32)); + } + } + + const key = TypeKey{ .words = try gpa.dupe(Word, key_words.items) }; + const gop = try canonical_map.getOrPut(gpa, key); + if (gop.found_existing) { + try id_remap.put(gpa, result_id, gop.value_ptr.*); + gpa.free(key.words); + } else { + gop.value_ptr.* = result_id; + } + } + + if (id_remap.count() == 0) return; + + // second pass: rewrite id references, remove duplicates and redundant annotations + var new_words: std.ArrayList(Word) = .empty; + defer new_words.deinit(gpa); + try new_words.ensureTotalCapacity(gpa, binary.instructions.len); + + var emitted_annotations: std.AutoHashMapUnmanaged(u64, void) = .empty; + defer emitted_annotations.deinit(gpa); + + var new_functions_offset: ?usize = null; + var max_id: Word = 0; + + it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (new_functions_offset == null and inst.offset >= binary.functions_start) { + new_functions_offset = new_words.items.len; + } + + if (canDeduplicate(inst.opcode)) { + const result_id_index: usize = switch (inst.opcode.class()) { + .type_declaration, .extension => 0, + .constant_creation => 1, + else => unreachable, + }; + if (result_id_index < inst.operands.len) { + const result_id: Id = @enumFromInt(inst.operands[result_id_index]); + if (id_remap.contains(result_id)) continue; + } + } + + switch (inst.opcode.class()) { + .annotation, .debug => { + if (inst.operands.len > 0) { + const target: Id = @enumFromInt(inst.operands[0]); + if (id_remap.contains(target)) continue; + } + }, + else => {}, + } + + const inst_start = new_words.items.len; + new_words.appendAssumeCapacity(binary.instructions[inst.offset]); + new_words.appendSliceAssumeCapacity(inst.operands); + const inst_slice = new_words.items[inst_start + 1 ..]; + + id_offsets.items.len = 0; + parser.parseInstructionResultIds(binary.*, inst, &id_offsets) catch continue; + + const inst_spec = parser.getInstSpec(inst.opcode); + const maybe_result_id_index: ?usize = if (inst_spec) |ispec| blk: { + break :blk for (0..@min(2, ispec.operands.len)) |i| { + if (ispec.operands[i].kind == .id_result) break @intCast(i); + } else null; + } else null; + + for (inst_slice, 0..) |*word, i| { + if (std.mem.indexOfScalar(u16, id_offsets.items, @intCast(i)) == null) continue; + max_id = @max(max_id, word.*); + if (maybe_result_id_index != null and i == maybe_result_id_index.?) continue; + + if (id_remap.get(@enumFromInt(word.*))) |canonical| { + word.* = @intFromEnum(canonical); + max_id = @max(max_id, word.*); + } + } + + switch (inst.opcode.class()) { + .annotation, .debug => { + const ann_words = new_words.items[inst_start..]; + const ann_hash = std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(ann_words)); + const gop = try emitted_annotations.getOrPut(gpa, ann_hash); + if (gop.found_existing) { + new_words.items.len = inst_start; + continue; + } + }, + else => {}, + } + } + + var remap_it = id_remap.iterator(); + while (remap_it.next()) |entry| { + _ = binary.ext_inst_map.remove(entry.key_ptr.*); + _ = binary.arith_type_width.remove(entry.key_ptr.*); + } + + binary.instructions = try gpa.dupe(Word, new_words.items); + binary.functions_start = new_functions_offset orelse new_words.items.len; + binary.id_bound = max_id + 1; +} + +fn canDeduplicate(opcode: Opcode) bool { + return switch (opcode) { + .OpTypeForwardPointer => false, + .OpGroupDecorate, .OpGroupMemberDecorate => false, + else => switch (opcode.class()) { + .type_declaration, .constant_creation => true, + .extension => opcode == .OpExtInstImport, + else => false, + }, + }; +} + +const TypeKey = struct { + words: []const Word, + + const HashContext = struct { + pub fn hash(_: @This(), key: TypeKey) u32 { + return @truncate(std.hash.Wyhash.hash(0, std.mem.sliceAsBytes(key.words))); + } + + pub fn eql(_: @This(), a: TypeKey, b: TypeKey, _: usize) bool { + return std.mem.eql(Word, a.words, b.words); + } + }; +}; diff --git a/src/link/SpirV/lower_invocation_globals.zig b/src/link/SpirV/lower_invocation_globals.zig index 6761047afaee66af804d2435a68df54257f86a88..c4fef691f51408da4f090e030840574ea670603b 100644 --- a/src/link/SpirV/lower_invocation_globals.zig +++ b/src/link/SpirV/lower_invocation_globals.zig @@ -71,7 +71,7 @@ const ModuleInfo = struct { arena: Allocator, parser: *BinaryModule.Parser, binary: BinaryModule, - ) BinaryModule.ParseError!ModuleInfo { + ) !ModuleInfo { var entry_points: std.array_hash_map.Auto(ResultId, void) = .empty; var functions: std.array_hash_map.Auto(ResultId, Fn) = .empty; var fn_types = std.AutoHashMap(ResultId, struct { @@ -79,9 +79,9 @@ const ModuleInfo = struct { param_types: []const ResultId, }).init(arena); var calls: std.array_hash_map.Auto(ResultId, void) = .empty; - var callee_store = std.array_list.Managed(ResultId).init(arena); + var callee_store: std.ArrayList(ResultId) = .empty; var function_invocation_globals: std.array_hash_map.Auto(ResultId, void) = .empty; - var result_id_offsets = std.array_list.Managed(u16).init(arena); + var result_id_offsets: std.ArrayList(u16) = .empty; var invocation_globals: std.array_hash_map.Auto(ResultId, InvocationGlobal) = .empty; var maybe_current_function: ?ResultId = null; @@ -164,7 +164,7 @@ const ModuleInfo = struct { } const first_callee = callee_store.items.len; - try callee_store.appendSlice(calls.keys()); + try callee_store.appendSlice(arena, calls.keys()); const fn_type = fn_types.get(fn_ty_id) orelse { log.err("Function {f} has invalid OpFunction type", .{current_function}); @@ -395,12 +395,12 @@ const ModuleBuilder = struct { return @enumFromInt(self.id_bound); } - fn finalize(self: *ModuleBuilder, a: Allocator, binary: *BinaryModule) !void { + fn finalize(self: *ModuleBuilder, arena: Allocator, binary: *BinaryModule) !void { binary.id_bound = self.id_bound; - binary.instructions = try a.dupe(Word, self.section.instructions.items); + binary.instructions = try arena.dupe(Word, self.section.instructions.items); // Nothing is removed in this pass so we don't need to change any of the maps, // just make sure the section is updated. - binary.sections.functions = self.new_functions_section orelse binary.instructions.len; + binary.functions_start = self.new_functions_section orelse binary.instructions.len; } /// Process everything from `binary` up to the first function and emit it into the builder. @@ -525,12 +525,12 @@ const ModuleBuilder = struct { binary: BinaryModule, info: ModuleInfo, ) !void { - var result_id_offsets = std.array_list.Managed(u16).init(self.arena); - var operands = std.array_list.Managed(u32).init(self.arena); + var result_id_offsets: std.ArrayList(u16) = .empty; + var operands: std.ArrayList(u32) = .empty; var maybe_current_function: ?ResultId = null; var skip_until_end: bool = false; - var it = binary.iterateInstructionsFrom(binary.sections.functions); + var it = binary.iterateInstructionsFrom(binary.functions_start); self.new_functions_section = self.section.instructions.items.len; while (it.next()) |inst| { if (skip_until_end) { @@ -541,7 +541,7 @@ const ModuleBuilder = struct { try parser.parseInstructionResultIds(binary, inst, &result_id_offsets); operands.items.len = 0; - try operands.appendSlice(inst.operands); + try operands.appendSlice(self.arena, inst.operands); // Replace the result-ids with the global's new result-id if required. for (result_id_offsets.items) |off| { @@ -741,14 +741,14 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule, progress: std.Pr const sub_node = progress.start("Lower invocation globals", 6); defer sub_node.end(); - var arena = std.heap.ArenaAllocator.init(parser.a); - defer arena.deinit(); - const a = arena.allocator(); + var arena_state = std.heap.ArenaAllocator.init(parser.gpa); + defer arena_state.deinit(); + const arena = arena_state.allocator(); - var info = try ModuleInfo.parse(a, parser, binary.*); - try info.resolve(a); + var info = try ModuleInfo.parse(arena, parser, binary.*); + try info.resolve(arena); - var builder = try ModuleBuilder.init(a, binary.*, info); + var builder = try ModuleBuilder.init(arena, binary.*, info); sub_node.completeOne(); try builder.deriveNewFnInfo(info); sub_node.completeOne(); @@ -760,5 +760,5 @@ pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule, progress: std.Pr sub_node.completeOne(); try builder.emitNewEntryPoints(info); sub_node.completeOne(); - try builder.finalize(parser.a, binary); + try builder.finalize(parser.gpa, binary); } diff --git a/src/link/SpirV/prune_unused.zig b/src/link/SpirV/prune_unused.zig new file mode 100644 index 0000000000000000000000000000000000000000..9eec490155fda066669b548fc80fc2e55c68d176 --- /dev/null +++ b/src/link/SpirV/prune_unused.zig @@ -0,0 +1,234 @@ +const std = @import("std"); +const BinaryModule = @import("BinaryModule.zig"); +const spec = @import("../../codegen/spirv/spec.zig"); +const Opcode = spec.Opcode; +const ResultId = spec.Id; +const Word = spec.Word; + +pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule) !void { + const gpa = parser.gpa; + + // map result-id → index in id_offsets for preamble instructions and function headers + var id_to_index: std.AutoHashMapUnmanaged(ResultId, u32) = .empty; + defer id_to_index.deinit(gpa); + + // for each indexed instruction, its offset in the binary + var code_offsets: std.ArrayList(usize) = .empty; + defer code_offsets.deinit(gpa); + + var it = binary.iterateInstructions(); + while (it.next()) |inst| { + const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; + const result_id = getResultId(inst, inst_spec) orelse continue; + + // only index preamble instructions and function headers + if (inst.offset < binary.functions_start or inst.opcode == .OpFunction) { + const index: u32 = @intCast(code_offsets.items.len); + try id_to_index.put(gpa, result_id, index); + try code_offsets.append(gpa, inst.offset); + } + } + + var alive: std.bit_set.Dynamic = try .initEmpty(gpa, code_offsets.items.len); + defer alive.deinit(gpa); + + var id_offset_buf: std.ArrayList(u16) = .empty; + defer id_offset_buf.deinit(gpa); + + // mark non-prunable preamble instructions alive + it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (inst.offset >= binary.functions_start) break; + if (!canPrune(inst.opcode)) { + markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf) catch {}; + } + } + + // mark alive functions' contents alive + it = binary.iterateInstructionsFrom(binary.functions_start); + while (it.next()) |inst| { + if (inst.opcode == .OpFunction) { + const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; + const result_id = getResultId(inst, inst_spec) orelse continue; + const index = id_to_index.get(result_id) orelse continue; + if (!alive.isSet(index)) { + // skip dead function + while (it.next()) |inner| { + if (inner.opcode == .OpFunctionEnd) break; + } + continue; + } + } + + // mark operands of alive function contents + if (!canPrune(inst.opcode)) { + markAlive(parser, binary.*, inst, &alive, &id_to_index, &code_offsets, &id_offset_buf) catch {}; + } + } + + // rewrite + var new_words: std.ArrayList(Word) = .empty; + defer new_words.deinit(gpa); + try new_words.ensureTotalCapacity(gpa, binary.instructions.len); + + var new_functions_start: ?usize = null; + + it = binary.iterateInstructions(); + while (it.next()) |inst| { + if (inst.offset >= binary.functions_start and inst.opcode == .OpFunction) { + const inst_spec = parser.getInstSpec(inst.opcode) orelse continue; + const result_id = getResultId(inst, inst_spec) orelse continue; + const index = id_to_index.get(result_id) orelse continue; + if (!alive.isSet(index)) { + while (it.next()) |inner| { + if (inner.opcode == .OpFunctionEnd) break; + } + continue; + } + } + + if (canPrune(inst.opcode)) { + const inst_spec = parser.getInstSpec(inst.opcode) orelse { + appendInst(&new_words, binary, inst, &new_functions_start); + continue; + }; + + if (getResultId(inst, inst_spec)) |result_id| { + const index = id_to_index.get(result_id) orelse { + appendInst(&new_words, binary, inst, &new_functions_start); + continue; + }; + if (!alive.isSet(index)) continue; + } else { + // annotation-style: emit only if all id operands are alive + id_offset_buf.items.len = 0; + parser.parseInstructionResultIds(binary.*, inst, &id_offset_buf) catch continue; + var all_alive = true; + for (id_offset_buf.items) |off| { + const id: ResultId = @enumFromInt(inst.operands[off]); + if (id_to_index.get(id)) |idx| { + if (!alive.isSet(idx)) { + all_alive = false; + break; + } + } + } + if (!all_alive) continue; + } + } + + appendInst(&new_words, binary, inst, &new_functions_start); + } + + { + var to_remove: std.ArrayList(ResultId) = .empty; + defer to_remove.deinit(gpa); + + var ext_it = binary.ext_inst_map.iterator(); + while (ext_it.next()) |entry| { + if (id_to_index.get(entry.key_ptr.*)) |index| { + if (!alive.isSet(index)) try to_remove.append(gpa, entry.key_ptr.*); + } + } + for (to_remove.items) |id| _ = binary.ext_inst_map.remove(id); + + to_remove.items.len = 0; + var arith_it = binary.arith_type_width.iterator(); + while (arith_it.next()) |entry| { + if (id_to_index.get(entry.key_ptr.*)) |index| { + if (!alive.isSet(index)) try to_remove.append(gpa, entry.key_ptr.*); + } + } + for (to_remove.items) |id| _ = binary.arith_type_width.remove(id); + } + + binary.instructions = try gpa.dupe(Word, new_words.items); + binary.functions_start = new_functions_start orelse new_words.items.len; +} + +fn appendInst( + new_words: *std.ArrayList(Word), + binary: *const BinaryModule, + inst: BinaryModule.Instruction, + new_functions_start: *?usize, +) void { + if (new_functions_start.* == null and inst.offset >= binary.functions_start) { + new_functions_start.* = new_words.items.len; + } + const len = @as(usize, binary.instructions[inst.offset] >> 16); + new_words.appendSliceAssumeCapacity(binary.instructions[inst.offset..][0..len]); +} + +fn markAlive( + parser: *BinaryModule.Parser, + binary: BinaryModule, + inst: BinaryModule.Instruction, + alive: *std.DynamicBitSetUnmanaged, + id_to_index: *const std.AutoHashMapUnmanaged(ResultId, u32), + code_offsets: *const std.ArrayList(usize), + id_offset_buf: *std.ArrayList(u16), +) !void { + const start = id_offset_buf.items.len; + try parser.parseInstructionResultIds(binary, inst, id_offset_buf); + const end = id_offset_buf.items.len; + + var i = start; + while (i < end) : (i += 1) { + const off = id_offset_buf.items[i]; + const id: ResultId = @enumFromInt(inst.operands[off]); + const index = id_to_index.get(id) orelse continue; + if (alive.isSet(index)) continue; + alive.set(index); + + const offset = code_offsets.items[index]; + const ref_inst = BinaryModule.Instruction{ + .opcode = @enumFromInt(binary.instructions[offset] & 0xFFFF), + .offset = offset, + .operands = blk: { + const l = binary.instructions[offset] >> 16; + break :blk binary.instructions[offset..][1..l]; + }, + }; + + if (ref_inst.opcode == .OpFunction) { + var fn_it = binary.iterateInstructionsFrom(ref_inst.offset); + _ = fn_it.next(); + while (fn_it.next()) |fn_inst| { + if (fn_inst.opcode == .OpFunctionEnd) break; + markAlive(parser, binary, fn_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {}; + } + markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {}; + } else { + markAlive(parser, binary, ref_inst, alive, id_to_index, code_offsets, id_offset_buf) catch {}; + } + } +} + +fn getResultId(inst: BinaryModule.Instruction, inst_spec: spec.Instruction) ?ResultId { + for (0..@min(2, inst_spec.operands.len)) |i| { + if (inst_spec.operands[i].kind == .id_result) { + if (i < inst.operands.len) return @enumFromInt(inst.operands[i]); + } + } + return null; +} + +fn canPrune(op: Opcode) bool { + return switch (op.class()) { + .type_declaration, + .constant_creation, + .annotation, + => true, + else => switch (op) { + .OpFunction, + .OpUndef, + .OpString, + .OpName, + .OpMemberName, + .OpExtInstImport, + .OpVariable, + => true, + else => false, + }, + }; +} diff --git a/src/target.zig b/src/target.zig index 761c5201cbd17082de092066bf71415bf3cc1fa3..b33842b16f74702d60f5d3061bbe9e56f364b05d 100644 --- a/src/target.zig +++ b/src/target.zig @@ -953,9 +953,7 @@ pub inline fn backendSupportsFeature(backend: std.lang.CompilerBackend, comptime // threads because they would all just be locking the same mutex to // protect Builder. .stage2_llvm => false, - // Same problem. Frontend needs to allow this backend to run in the - // linker thread. - .stage2_spirv => false, + .stage2_spirv => true, // Please do not make any more exceptions. Backends must support // being run in a separate thread from now on. else => true, diff --git a/test/behavior/align.zig b/test/behavior/align.zig index 7306ef388f0d534bd8a11a14dffd49d94128aec6..8f96d5cd8a429280210926953bf3fbf763065910 100644 --- a/test/behavior/align.zig +++ b/test/behavior/align.zig @@ -639,6 +639,8 @@ test "function pointer align mask" { } test "align expression is implicitly comptime" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + const S = struct { fn alignment() usize { return 4; diff --git a/test/behavior/enum.zig b/test/behavior/enum.zig index fb5ce0623bf8e1be1a12889da094d7371e118536..bd1010f6dc8b7738d6597774010a43ef63fde9ad 100644 --- a/test/behavior/enum.zig +++ b/test/behavior/enum.zig @@ -1318,6 +1318,8 @@ test "switch on an extern enum with negative value" { } test "switch on an enum with small signed tag type" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + const E = enum(i3) { y = -2, z = -1, diff --git a/test/behavior/inline_switch.zig b/test/behavior/inline_switch.zig index 02408f74b95eb214b03ee81f08e4b76c834808cb..68f303c93d558fec3cce0482c7d1e9399d21d25d 100644 --- a/test/behavior/inline_switch.zig +++ b/test/behavior/inline_switch.zig @@ -4,6 +4,7 @@ const builtin = @import("builtin"); test "inline scalar prongs" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; var x: usize = 0; switch (x) { @@ -18,6 +19,7 @@ test "inline scalar prongs" { test "inline prong ranges" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; var x: usize = 0; _ = &x; @@ -32,6 +34,7 @@ test "inline prong ranges" { const E = enum { a, b, c, d }; test "inline switch enums" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; var x: E = .a; _ = &x; @@ -71,6 +74,7 @@ test "inline switch unions" { test "inline else bool" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; var a = true; _ = &a; @@ -82,6 +86,7 @@ test "inline else bool" { test "inline else error" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; const Err = error{ a, b, c }; var a = Err.a; @@ -94,6 +99,7 @@ test "inline else error" { test "inline else enum" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; const E2 = enum(u8) { a = 2, b = 3, c = 4, d = 5 }; var a: E2 = .a; @@ -124,6 +130,7 @@ test "inline else int with gaps" { test "inline else int all values" { if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; var a: u2 = 0; _ = &a; diff --git a/test/behavior/ir_block_deps.zig b/test/behavior/ir_block_deps.zig index 4708af87f4bd27060125a8e2c0e111e0996badab..f6ed6bb53f0d5e961602d787db1e7895f4de1a18 100644 --- a/test/behavior/ir_block_deps.zig +++ b/test/behavior/ir_block_deps.zig @@ -20,6 +20,7 @@ fn getErrInt() anyerror!i32 { test "ir block deps" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; try expect((foo(1) catch unreachable) == 0); try expect((foo(2) catch unreachable) == 0); diff --git a/test/behavior/switch.zig b/test/behavior/switch.zig index d9e581521b9a66851a0d4966c2df3e1f8975a6a2..1e88b96582c1344d1d06627ca0cc5d9d4501a778 100644 --- a/test/behavior/switch.zig +++ b/test/behavior/switch.zig @@ -801,6 +801,7 @@ test "enum value without tag name used as switch item" { } test "switch item sizeof" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; const S = struct { fn doTheTest() !void { var a: usize = 0; diff --git a/test/behavior/tuple.zig b/test/behavior/tuple.zig index 0876886b75f04feaba0462975ef8baedd8ecfa38..2899d5b366c5d5d264a069d1839f64f62839ce7f 100644 --- a/test/behavior/tuple.zig +++ b/test/behavior/tuple.zig @@ -299,6 +299,7 @@ test "tuple type with void field and a runtime field" { test "branching inside tuple literal" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; const S = struct { fn foo(a: anytype) !void { diff --git a/test/behavior/union.zig b/test/behavior/union.zig index 163eb8f3f6a5989e9fb1c3cc9baba1c877222c90..136d426e8c82d82c783c12a272d84daccd6794d9 100644 --- a/test/behavior/union.zig +++ b/test/behavior/union.zig @@ -885,6 +885,8 @@ test "union no tag with struct member" { } test "extern union doesn't trigger field check at comptime" { + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; + const U = extern union { x: u32, y: u8, @@ -1214,6 +1216,7 @@ test "return an extern union from C calling convention" { test "noreturn field in union" { if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO + if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest; const U = union(enum) { a: u32, diff --git a/test/cases/compile_errors/illegal_operation_on_logical_ptr.zig b/test/cases/compile_errors/illegal_operation_on_logical_ptr.zig index 85decd8a6c29fb74f56ffefcc5bb99d3fd5e2339..859f0be6d37f1961be9f2bc295daeb330858b3cd 100644 --- a/test/cases/compile_errors/illegal_operation_on_logical_ptr.zig +++ b/test/cases/compile_errors/illegal_operation_on_logical_ptr.zig @@ -1,15 +1,3 @@ -export fn elemPtr() void { - var ptr: [*]u8 = undefined; - ptr[0] = 0; -} - -export fn elemVal() void { - var ptr: [*]u8 = undefined; - var val = ptr[0]; - _ = &ptr; - _ = &val; -} - export fn intFromPtr() void { var value: u8 = 0; _ = @intFromPtr(&value); @@ -37,15 +25,11 @@ export fn ptrIntArithmetic() void { // error // target=spirv64-vulkan // -// :3:8: error: illegal operation on logical pointer of type '[*]u8' -// :3:8: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -// :8:18: error: illegal operation on logical pointer of type '[*]u8' -// :8:18: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -// :15:21: error: illegal operation on logical pointer of type '*u8' -// :15:21: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -// :20:20: error: illegal operation on logical pointer of type '*u8' -// :20:20: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -// :28:17: error: illegal operation on logical pointer of type '*u8' -// :28:17: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -// :34:14: error: illegal operation on logical pointer of type '[*]u8' -// :34:14: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan +// :3:21: error: illegal operation on logical pointer of type '*u8' +// :3:21: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan +// :8:20: error: illegal operation on logical pointer of type '*u8' +// :8:20: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan +// :16:17: error: illegal operation on logical pointer of type '*u8' +// :16:17: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan +// :22:14: error: illegal operation on logical pointer of type '[*]u8' +// :22:14: note: cannot perform arithmetic on pointers with address space 'generic' on target spirv-vulkan -- 2.54.0