Merge pull request #24661 from alichraghi/spv4

spirv: refactor and remove deduplication ISel
This commit is contained in:
Andrew Kelley 2025-08-07 20:55:50 -07:00 committed by GitHub
commit 3fb86841cc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 8245 additions and 12002 deletions

View File

@ -553,11 +553,6 @@ set(ZIG_STAGE2_SOURCES
src/codegen/c/Type.zig src/codegen/c/Type.zig
src/codegen/llvm.zig src/codegen/llvm.zig
src/codegen/llvm/bindings.zig src/codegen/llvm/bindings.zig
src/codegen/spirv.zig
src/codegen/spirv/Assembler.zig
src/codegen/spirv/Module.zig
src/codegen/spirv/Section.zig
src/codegen/spirv/spec.zig
src/crash_report.zig src/crash_report.zig
src/dev.zig src/dev.zig
src/libs/freebsd.zig src/libs/freebsd.zig
@ -620,11 +615,6 @@ set(ZIG_STAGE2_SOURCES
src/link/Plan9.zig src/link/Plan9.zig
src/link/Plan9/aout.zig src/link/Plan9/aout.zig
src/link/Queue.zig src/link/Queue.zig
src/link/SpirV.zig
src/link/SpirV/BinaryModule.zig
src/link/SpirV/deduplicate.zig
src/link/SpirV/lower_invocation_globals.zig
src/link/SpirV/prune_unused.zig
src/link/StringTable.zig src/link/StringTable.zig
src/link/Wasm.zig src/link/Wasm.zig
src/link/Wasm/Archive.zig src/link/Wasm/Archive.zig

View File

@ -177,7 +177,13 @@ const Os = switch (builtin.os.tag) {
const gop = try w.dir_table.getOrPut(gpa, path); const gop = try w.dir_table.getOrPut(gpa, path);
if (!gop.found_existing) { if (!gop.found_existing) {
var mount_id: MountId = undefined; var mount_id: MountId = undefined;
const dir_handle = try Os.getDirHandle(gpa, path, &mount_id); const dir_handle = Os.getDirHandle(gpa, path, &mount_id) catch |err| switch (err) {
error.FileNotFound => {
std.debug.assert(w.dir_table.swapRemove(path));
continue;
},
else => return err,
};
const fan_fd = blk: { const fan_fd = blk: {
const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id); const fd_gop = try w.os.poll_fds.getOrPut(gpa, mount_id);
if (!fd_gop.found_existing) { if (!fd_gop.found_existing) {

View File

@ -3651,9 +3651,8 @@ pub fn errorSetBits(zcu: *const Zcu) u16 {
if (zcu.error_limit == 0) return 0; if (zcu.error_limit == 0) return 0;
if (target.cpu.arch.isSpirV()) { if (target.cpu.arch.isSpirV()) {
if (!target.cpu.has(.spirv, .storage_push_constant16)) { // As expected by https://github.com/Snektron/zig-spirv-test-executor
return 32; if (zcu.comp.config.is_test) return 32;
}
} }
return @as(u16, std.math.log2_int(ErrorInt, zcu.error_limit)) + 1; return @as(u16, std.math.log2_int(ErrorInt, zcu.error_limit)) + 1;

View File

@ -4459,13 +4459,10 @@ fn runCodegenInner(pt: Zcu.PerThread, func_index: InternPool.Index, air: *Air) e
const lf = comp.bin_file orelse return error.NoLinkFile; const lf = comp.bin_file orelse return error.NoLinkFile;
// TODO: self-hosted codegen should always have a type of MIR; codegen should produce that MIR, // Just like LLVM, the SPIR-V backend can't multi-threaded due to SPIR-V design limitations.
// and the linker should consume it. However, our SPIR-V backend is currently tightly coupled
// with our SPIR-V linker, so needs to work more like the LLVM backend. This should be fixed to
// unblock threaded codegen for SPIR-V.
if (lf.cast(.spirv)) |spirv_file| { if (lf.cast(.spirv)) |spirv_file| {
assert(pt.tid == .main); // SPIR-V has a lot of shared state assert(pt.tid == .main); // SPIR-V has a lot of shared state
spirv_file.object.updateFunc(pt, func_index, air, &liveness) catch |err| { spirv_file.updateFunc(pt, func_index, air, &liveness) catch |err| {
switch (err) { switch (err) {
error.OutOfMemory => comp.link_diags.setAllocFailure(), error.OutOfMemory => comp.link_diags.setAllocFailure(),
} }

View File

@ -57,7 +57,7 @@ fn importBackend(comptime backend: std.builtin.CompilerBackend) type {
.stage2_powerpc => unreachable, .stage2_powerpc => unreachable,
.stage2_riscv64 => @import("arch/riscv64/CodeGen.zig"), .stage2_riscv64 => @import("arch/riscv64/CodeGen.zig"),
.stage2_sparc64 => @import("arch/sparc64/CodeGen.zig"), .stage2_sparc64 => @import("arch/sparc64/CodeGen.zig"),
.stage2_spirv => @import("codegen/spirv.zig"), .stage2_spirv => @import("codegen/spirv/CodeGen.zig"),
.stage2_wasm => @import("arch/wasm/CodeGen.zig"), .stage2_wasm => @import("arch/wasm/CodeGen.zig"),
.stage2_x86, .stage2_x86_64 => @import("arch/x86_64/CodeGen.zig"), .stage2_x86, .stage2_x86_64 => @import("arch/x86_64/CodeGen.zig"),
_ => unreachable, _ => unreachable,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -13,8 +13,6 @@ const Log2Word = std.math.Log2Int(Word);
const Opcode = spec.Opcode; const Opcode = spec.Opcode;
/// The instructions in this section. Memory is owned by the Module
/// externally associated to this Section.
instructions: std.ArrayListUnmanaged(Word) = .empty, instructions: std.ArrayListUnmanaged(Word) = .empty,
pub fn deinit(section: *Section, allocator: Allocator) void { pub fn deinit(section: *Section, allocator: Allocator) void {
@ -22,9 +20,8 @@ pub fn deinit(section: *Section, allocator: Allocator) void {
section.* = undefined; section.* = undefined;
} }
/// Clear the instructions in this section
pub fn reset(section: *Section) void { pub fn reset(section: *Section) void {
section.instructions.items.len = 0; section.instructions.clearRetainingCapacity();
} }
pub fn toWords(section: Section) []Word { pub fn toWords(section: Section) []Word {
@ -36,9 +33,12 @@ pub fn append(section: *Section, allocator: Allocator, other_section: Section) !
try section.instructions.appendSlice(allocator, other_section.instructions.items); try section.instructions.appendSlice(allocator, other_section.instructions.items);
} }
/// Ensure capacity of at least `capacity` more words in this section. pub fn ensureUnusedCapacity(
pub fn ensureUnusedCapacity(section: *Section, allocator: Allocator, capacity: usize) !void { section: *Section,
try section.instructions.ensureUnusedCapacity(allocator, capacity); allocator: Allocator,
words: usize,
) !void {
try section.instructions.ensureUnusedCapacity(allocator, words);
} }
/// Write an instruction and size, operands are to be inserted manually. /// Write an instruction and size, operands are to be inserted manually.
@ -46,7 +46,7 @@ pub fn emitRaw(
section: *Section, section: *Section,
allocator: Allocator, allocator: Allocator,
opcode: Opcode, opcode: Opcode,
operand_words: usize, // opcode itself not included operand_words: usize,
) !void { ) !void {
const word_count = 1 + operand_words; const word_count = 1 + operand_words;
try section.instructions.ensureUnusedCapacity(allocator, word_count); try section.instructions.ensureUnusedCapacity(allocator, word_count);
@ -64,6 +64,16 @@ pub fn emitRawInstruction(
section.writeWords(operands); section.writeWords(operands);
} }
pub fn emitAssumeCapacity(
section: *Section,
comptime opcode: spec.Opcode,
operands: opcode.Operands(),
) !void {
const word_count = instructionSize(opcode, operands);
section.writeWord(@as(Word, @intCast(word_count << 16)) | @intFromEnum(opcode));
section.writeOperands(opcode.Operands(), operands);
}
pub fn emit( pub fn emit(
section: *Section, section: *Section,
allocator: Allocator, allocator: Allocator,
@ -76,35 +86,6 @@ pub fn emit(
section.writeOperands(opcode.Operands(), operands); section.writeOperands(opcode.Operands(), operands);
} }
pub fn emitBranch(
section: *Section,
allocator: Allocator,
target_label: spec.Id,
) !void {
try section.emit(allocator, .OpBranch, .{
.target_label = target_label,
});
}
pub fn emitSpecConstantOp(
section: *Section,
allocator: Allocator,
comptime opcode: spec.Opcode,
operands: opcode.Operands(),
) !void {
const word_count = operandsSize(opcode.Operands(), operands);
try section.emitRaw(allocator, .OpSpecConstantOp, 1 + word_count);
section.writeOperand(spec.Id, operands.id_result_type);
section.writeOperand(spec.Id, operands.id_result);
section.writeOperand(Opcode, opcode);
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 { pub fn writeWord(section: *Section, word: Word) void {
section.instructions.appendAssumeCapacity(word); section.instructions.appendAssumeCapacity(word);
} }
@ -126,7 +107,6 @@ fn writeOperands(section: *Section, comptime Operands: type, operands: Operands)
.void => return, .void => return,
else => unreachable, else => unreachable,
}; };
inline for (fields) |field| { inline for (fields) |field| {
section.writeOperand(field.type, @field(operands, field.name)); section.writeOperand(field.type, @field(operands, field.name));
} }
@ -134,30 +114,18 @@ fn writeOperands(section: *Section, comptime Operands: type, operands: Operands)
pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void { pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand) void {
switch (Operand) { switch (Operand) {
spec.LiteralSpecConstantOpInteger => unreachable,
spec.Id => section.writeWord(@intFromEnum(operand)), spec.Id => section.writeWord(@intFromEnum(operand)),
spec.LiteralInteger => section.writeWord(operand), spec.LiteralInteger => section.writeWord(operand),
spec.LiteralString => section.writeString(operand), spec.LiteralString => section.writeString(operand),
spec.LiteralContextDependentNumber => section.writeContextDependentNumber(operand), spec.LiteralContextDependentNumber => section.writeContextDependentNumber(operand),
spec.LiteralExtInstInteger => section.writeWord(operand.inst), spec.LiteralExtInstInteger => section.writeWord(operand.inst),
// TODO: Where this type is used (OpSpecConstantOp) is currently not correct in the spec json,
// so it most likely needs to be altered into something that can actually describe the entire
// instruction in which it is used.
spec.LiteralSpecConstantOpInteger => section.writeWord(@intFromEnum(operand.opcode)),
spec.PairLiteralIntegerIdRef => section.writeWords(&.{ operand.value, @enumFromInt(operand.label) }), spec.PairLiteralIntegerIdRef => section.writeWords(&.{ operand.value, @enumFromInt(operand.label) }),
spec.PairIdRefLiteralInteger => section.writeWords(&.{ @intFromEnum(operand.target), operand.member }), spec.PairIdRefLiteralInteger => section.writeWords(&.{ @intFromEnum(operand.target), operand.member }),
spec.PairIdRefIdRef => section.writeWords(&.{ @intFromEnum(operand[0]), @intFromEnum(operand[1]) }), spec.PairIdRefIdRef => section.writeWords(&.{ @intFromEnum(operand[0]), @intFromEnum(operand[1]) }),
else => switch (@typeInfo(Operand)) { else => switch (@typeInfo(Operand)) {
.@"enum" => section.writeWord(@intFromEnum(operand)), .@"enum" => section.writeWord(@intFromEnum(operand)),
.optional => |info| if (operand) |child| { .optional => |info| if (operand) |child| section.writeOperand(info.child, child),
section.writeOperand(info.child, child);
},
.pointer => |info| { .pointer => |info| {
std.debug.assert(info.size == .slice); // Should be no other pointer types in the spec. std.debug.assert(info.size == .slice); // Should be no other pointer types in the spec.
for (operand) |item| { for (operand) |item| {
@ -178,18 +146,14 @@ pub fn writeOperand(section: *Section, comptime Operand: type, operand: Operand)
} }
fn writeString(section: *Section, str: []const u8) void { fn writeString(section: *Section, str: []const u8) void {
// TODO: Not actually sure whether this is correct for big-endian.
// See https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#Literal
const zero_terminated_len = str.len + 1; const zero_terminated_len = str.len + 1;
var i: usize = 0; var i: usize = 0;
while (i < zero_terminated_len) : (i += @sizeOf(Word)) { while (i < zero_terminated_len) : (i += @sizeOf(Word)) {
var word: Word = 0; var word: Word = 0;
var j: usize = 0; var j: usize = 0;
while (j < @sizeOf(Word) and i + j < str.len) : (j += 1) { while (j < @sizeOf(Word) and i + j < str.len) : (j += 1) {
word |= @as(Word, str[i + j]) << @as(Log2Word, @intCast(j * @bitSizeOf(u8))); word |= @as(Word, str[i + j]) << @as(Log2Word, @intCast(j * @bitSizeOf(u8)));
} }
section.instructions.appendAssumeCapacity(word); section.instructions.appendAssumeCapacity(word);
} }
} }
@ -233,20 +197,19 @@ fn writeExtendedMask(section: *Section, comptime Operand: type, operand: Operand
} }
fn writeExtendedUnion(section: *Section, comptime Operand: type, operand: Operand) void { fn writeExtendedUnion(section: *Section, comptime Operand: type, operand: Operand) void {
const tag = std.meta.activeTag(operand); return switch (operand) {
section.writeWord(@intFromEnum(tag)); inline else => |op, tag| {
section.writeWord(@intFromEnum(tag));
inline for (@typeInfo(Operand).@"union".fields) |field| { section.writeOperands(
if (@field(Operand, field.name) == tag) { @FieldType(Operand, @tagName(tag)),
section.writeOperands(field.type, @field(operand, field.name)); op,
return; );
} },
} };
unreachable;
} }
fn instructionSize(comptime opcode: spec.Opcode, operands: opcode.Operands()) usize { fn instructionSize(comptime opcode: spec.Opcode, operands: opcode.Operands()) usize {
return 1 + operandsSize(opcode.Operands(), operands); return operandsSize(opcode.Operands(), operands) + 1;
} }
fn operandsSize(comptime Operands: type, operands: Operands) usize { fn operandsSize(comptime Operands: type, operands: Operands) usize {
@ -266,28 +229,14 @@ fn operandsSize(comptime Operands: type, operands: Operands) usize {
fn operandSize(comptime Operand: type, operand: Operand) usize { fn operandSize(comptime Operand: type, operand: Operand) usize {
return switch (Operand) { return switch (Operand) {
spec.Id, spec.LiteralSpecConstantOpInteger => unreachable,
spec.LiteralInteger, spec.Id, spec.LiteralInteger, spec.LiteralExtInstInteger => 1,
spec.LiteralExtInstInteger, spec.LiteralString => std.math.divCeil(usize, operand.len + 1, @sizeOf(Word)) catch unreachable,
=> 1,
spec.LiteralString => std.math.divCeil(usize, operand.len + 1, @sizeOf(Word)) catch unreachable, // Add one for zero-terminator
spec.LiteralContextDependentNumber => switch (operand) { spec.LiteralContextDependentNumber => switch (operand) {
.int32, .uint32, .float32 => 1, .int32, .uint32, .float32 => 1,
.int64, .uint64, .float64 => 2, .int64, .uint64, .float64 => 2,
}, },
spec.PairLiteralIntegerIdRef, spec.PairIdRefLiteralInteger, spec.PairIdRefIdRef => 2,
// TODO: Where this type is used (OpSpecConstantOp) is currently not correct in the spec
// json, so it most likely needs to be altered into something that can actually
// describe the entire insturction in which it is used.
spec.LiteralSpecConstantOpInteger => 1,
spec.PairLiteralIntegerIdRef,
spec.PairIdRefLiteralInteger,
spec.PairIdRefIdRef,
=> 2,
else => switch (@typeInfo(Operand)) { else => switch (@typeInfo(Operand)) {
.@"enum" => 1, .@"enum" => 1,
.optional => |info| if (operand) |child| operandSize(info.child, child) else 0, .optional => |info| if (operand) |child| operandSize(info.child, child) else 0,
@ -299,133 +248,25 @@ fn operandSize(comptime Operand: type, operand: Operand) usize {
} }
break :blk total; break :blk total;
}, },
.@"struct" => |info| if (info.layout == .@"packed") 1 else extendedMaskSize(Operand, operand), .@"struct" => |struct_info| {
.@"union" => extendedUnionSize(Operand, operand), if (struct_info.layout == .@"packed") return 1;
var total: usize = 0;
inline for (@typeInfo(Operand).@"struct".fields) |field| {
switch (@typeInfo(field.type)) {
.optional => |info| if (@field(operand, field.name)) |child| {
total += operandsSize(info.child, child);
},
.bool => {},
else => unreachable,
}
}
return total + 1; // Add one for the mask itself.
},
.@"union" => switch (operand) {
inline else => |op, tag| operandsSize(@FieldType(Operand, @tagName(tag)), op) + 1,
},
else => unreachable, else => unreachable,
}, },
}; };
} }
fn extendedMaskSize(comptime Operand: type, operand: Operand) usize {
var total: usize = 0;
var any_set = false;
inline for (@typeInfo(Operand).@"struct".fields) |field| {
switch (@typeInfo(field.type)) {
.optional => |info| if (@field(operand, field.name)) |child| {
total += operandsSize(info.child, child);
any_set = true;
},
.bool => if (@field(operand, field.name)) {
any_set = true;
},
else => unreachable,
}
}
return total + 1; // Add one for the mask itself.
}
fn extendedUnionSize(comptime Operand: type, operand: Operand) usize {
const tag = std.meta.activeTag(operand);
inline for (@typeInfo(Operand).@"union".fields) |field| {
if (@field(Operand, field.name) == tag) {
// Add one for the tag itself.
return 1 + operandsSize(field.type, @field(operand, field.name));
}
}
unreachable;
}
test "SPIR-V Section emit() - no operands" {
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpNop, {});
try testing.expect(section.instructions.items[0] == (@as(Word, 1) << 16) | @intFromEnum(Opcode.OpNop));
}
test "SPIR-V Section emit() - simple" {
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpUndef, .{
.id_result_type = @enumFromInt(0),
.id_result = @enumFromInt(1),
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 3) << 16) | @intFromEnum(Opcode.OpUndef),
0,
1,
}, section.instructions.items);
}
test "SPIR-V Section emit() - string" {
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpSource, .{
.source_language = .Unknown,
.version = 123,
.file = @enumFromInt(256),
.source = "pub fn main() void {}",
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 10) << 16) | @intFromEnum(Opcode.OpSource),
@intFromEnum(spec.SourceLanguage.Unknown),
123,
456,
std.mem.bytesToValue(Word, "pub "),
std.mem.bytesToValue(Word, "fn m"),
std.mem.bytesToValue(Word, "ain("),
std.mem.bytesToValue(Word, ") vo"),
std.mem.bytesToValue(Word, "id {"),
std.mem.bytesToValue(Word, "}\x00\x00\x00"),
}, section.instructions.items);
}
test "SPIR-V Section emit() - extended mask" {
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpLoopMerge, .{
.merge_block = @enumFromInt(10),
.continue_target = @enumFromInt(20),
.loop_control = .{
.Unroll = true,
.DependencyLength = .{
.literal_integer = 2,
},
},
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 5) << 16) | @intFromEnum(Opcode.OpLoopMerge),
10,
20,
@as(Word, @bitCast(spec.LoopControl{ .Unroll = true, .DependencyLength = true })),
2,
}, section.instructions.items);
}
test "SPIR-V Section emit() - extended union" {
var section = Section{};
defer section.deinit(std.testing.allocator);
try section.emit(std.testing.allocator, .OpExecutionMode, .{
.entry_point = @enumFromInt(888),
.mode = .{
.LocalSize = .{ .x_size = 4, .y_size = 8, .z_size = 16 },
},
});
try testing.expectEqualSlices(Word, &.{
(@as(Word, 6) << 16) | @intFromEnum(Opcode.OpExecutionMode),
888,
@intFromEnum(spec.ExecutionMode.LocalSize),
4,
8,
16,
}, section.instructions.items);
}

File diff suppressed because it is too large Load Diff

View File

@ -191,6 +191,7 @@ pub const Env = enum {
.spirv => switch (feature) { .spirv => switch (feature) {
.spirv_backend, .spirv_backend,
.spirv_linker, .spirv_linker,
.legalize,
=> true, => true,
else => Env.sema.supports(feature), else => Env.sema.supports(feature),
}, },

View File

@ -1,62 +1,36 @@
//! SPIR-V Spec documentation: https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html
//! According to above documentation, a SPIR-V module has the following logical layout:
//! Header.
//! OpCapability instructions.
//! OpExtension instructions.
//! OpExtInstImport instructions.
//! A single OpMemoryModel instruction.
//! All entry points, declared with OpEntryPoint instructions.
//! All execution-mode declarators; OpExecutionMode and OpExecutionModeId instructions.
//! Debug instructions:
//! - First, OpString, OpSourceExtension, OpSource, OpSourceContinued (no forward references).
//! - OpName and OpMemberName instructions.
//! - OpModuleProcessed instructions.
//! All annotation (decoration) instructions.
//! All type declaration instructions, constant instructions, global variable declarations, (preferably) OpUndef instructions.
//! All function declarations without a body (extern functions presumably).
//! All regular functions.
// Because SPIR-V requires re-compilation anyway, and so hot swapping will not work
// anyway, we simply generate all the code in flush. This keeps
// things considerably simpler.
const SpirV = @This();
const std = @import("std"); const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const Path = std.Build.Cache.Path;
const assert = std.debug.assert; const assert = std.debug.assert;
const log = std.log.scoped(.link); const log = std.log.scoped(.link);
const Path = std.Build.Cache.Path;
const Zcu = @import("../Zcu.zig"); const Zcu = @import("../Zcu.zig");
const InternPool = @import("../InternPool.zig"); const InternPool = @import("../InternPool.zig");
const Compilation = @import("../Compilation.zig"); const Compilation = @import("../Compilation.zig");
const link = @import("../link.zig"); const link = @import("../link.zig");
const codegen = @import("../codegen/spirv.zig");
const trace = @import("../tracy.zig").trace;
const build_options = @import("build_options");
const Air = @import("../Air.zig"); const Air = @import("../Air.zig");
const Type = @import("../Type.zig"); const Type = @import("../Type.zig");
const Value = @import("../Value.zig"); const BinaryModule = @import("SpirV/BinaryModule.zig");
const CodeGen = @import("../codegen/spirv/CodeGen.zig");
const Module = @import("../codegen/spirv/Module.zig");
const trace = @import("../tracy.zig").trace;
const SpvModule = @import("../codegen/spirv/Module.zig");
const Section = @import("../codegen/spirv/Section.zig");
const spec = @import("../codegen/spirv/spec.zig"); const spec = @import("../codegen/spirv/spec.zig");
const Id = spec.Id; const Id = spec.Id;
const Word = spec.Word; const Word = spec.Word;
const BinaryModule = @import("SpirV/BinaryModule.zig"); const Linker = @This();
base: link.File, base: link.File,
module: Module,
object: codegen.Object, cg: CodeGen,
pub fn createEmpty( pub fn createEmpty(
arena: Allocator, arena: Allocator,
comp: *Compilation, comp: *Compilation,
emit: Path, emit: Path,
options: link.File.OpenOptions, options: link.File.OpenOptions,
) !*SpirV { ) !*Linker {
const gpa = comp.gpa; const gpa = comp.gpa;
const target = &comp.root_mod.resolved_target.result; const target = &comp.root_mod.resolved_target.result;
@ -72,8 +46,8 @@ pub fn createEmpty(
else => unreachable, // Caught by Compilation.Config.resolve. else => unreachable, // Caught by Compilation.Config.resolve.
} }
const self = try arena.create(SpirV); const linker = try arena.create(Linker);
self.* = .{ linker.* = .{
.base = .{ .base = .{
.tag = .spirv, .tag = .spirv,
.comp = comp, .comp = comp,
@ -85,17 +59,30 @@ pub fn createEmpty(
.file = null, .file = null,
.build_id = options.build_id, .build_id = options.build_id,
}, },
.object = codegen.Object.init(gpa, comp.getTarget()), .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 self.deinit(); errdefer linker.deinit();
// TODO: read the file and keep valid parts instead of truncating linker.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
self.base.file = try emit.root_dir.handle.createFile(emit.sub_path, .{
.truncate = true, .truncate = true,
.read = true, .read = true,
}); });
return self; return linker;
} }
pub fn open( pub fn open(
@ -103,27 +90,90 @@ pub fn open(
comp: *Compilation, comp: *Compilation,
emit: Path, emit: Path,
options: link.File.OpenOptions, options: link.File.OpenOptions,
) !*SpirV { ) !*Linker {
return createEmpty(arena, comp, emit, options); return createEmpty(arena, comp, emit, options);
} }
pub fn deinit(self: *SpirV) void { pub fn deinit(linker: *Linker) void {
self.object.deinit(); linker.cg.deinit();
linker.module.deinit();
} }
pub fn updateNav(self: *SpirV, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.File.UpdateNavError!void { fn generate(
if (build_options.skip_non_native) { linker: *Linker,
@panic("Attempted to compile for architecture that was disabled by build configuration"); 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.CodegenFail => switch (zcu.codegenFailMsg(nav_index, linker.cg.error_msg.?)) {
error.CodegenFail => {},
error.OutOfMemory => |e| return e,
},
else => |other| {
// There might be an error that happened *after* linker.error_msg
// was already allocated, so be sure to free it.
if (linker.cg.error_msg) |error_msg| {
error_msg.deinit(gpa);
}
return other;
},
};
}
pub fn updateFunc(
linker: *Linker,
pt: Zcu.PerThread,
func_index: InternPool.Index,
air: *const Air,
liveness: *const ?Air.Liveness,
) !void {
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);
}
pub fn updateNav(linker: *Linker, pt: Zcu.PerThread, nav: InternPool.Nav.Index) link.File.UpdateNavError!void {
const ip = &pt.zcu.intern_pool; const ip = &pt.zcu.intern_pool;
log.debug("lowering nav {f}({d})", .{ ip.getNav(nav).fqn.fmt(ip), nav }); log.debug("lowering nav {f}({d})", .{ ip.getNav(nav).fqn.fmt(ip), nav });
try linker.generate(pt, nav, undefined, undefined, false);
try self.object.updateNav(pt, nav);
} }
pub fn updateExports( pub fn updateExports(
self: *SpirV, linker: *Linker,
pt: Zcu.PerThread, pt: Zcu.PerThread,
exported: Zcu.Exported, exported: Zcu.Exported,
export_indices: []const Zcu.Export.Index, export_indices: []const Zcu.Export.Index,
@ -134,13 +184,13 @@ pub fn updateExports(
.nav => |nav| nav, .nav => |nav| nav,
.uav => |uav| { .uav => |uav| {
_ = uav; _ = uav;
@panic("TODO: implement SpirV linker code for exporting a constant value"); @panic("TODO: implement Linker linker code for exporting a constant value");
}, },
}; };
const nav_ty = ip.getNav(nav_index).typeOf(ip); const nav_ty = ip.getNav(nav_index).typeOf(ip);
const target = zcu.getTarget(); const target = zcu.getTarget();
if (ip.isFunctionType(nav_ty)) { if (ip.isFunctionType(nav_ty)) {
const spv_decl_index = try self.object.resolveNav(zcu, nav_index); const spv_decl_index = try linker.module.resolveNav(ip, nav_index);
const cc = Type.fromInterned(nav_ty).fnCallingConvention(zcu); const cc = Type.fromInterned(nav_ty).fnCallingConvention(zcu);
const exec_model: spec.ExecutionModel = switch (target.os.tag) { const exec_model: spec.ExecutionModel = switch (target.os.tag) {
.vulkan, .opengl => switch (cc) { .vulkan, .opengl => switch (cc) {
@ -162,7 +212,7 @@ pub fn updateExports(
for (export_indices) |export_idx| { for (export_indices) |export_idx| {
const exp = export_idx.ptr(zcu); const exp = export_idx.ptr(zcu);
try self.object.spv.declareEntryPoint( try linker.module.declareEntryPoint(
spv_decl_index, spv_decl_index,
exp.opts.name.toSlice(ip), exp.opts.name.toSlice(ip),
exec_model, exec_model,
@ -175,7 +225,7 @@ pub fn updateExports(
} }
pub fn flush( pub fn flush(
self: *SpirV, linker: *Linker,
arena: Allocator, arena: Allocator,
tid: Zcu.PerThread.Id, tid: Zcu.PerThread.Id,
prog_node: std.Progress.Node, prog_node: std.Progress.Node,
@ -185,35 +235,29 @@ pub fn flush(
// InternPool. // InternPool.
_ = tid; _ = tid;
if (build_options.skip_non_native) {
@panic("Attempted to compile for architecture that was disabled by build configuration");
}
const tracy = trace(@src()); const tracy = trace(@src());
defer tracy.end(); defer tracy.end();
const sub_prog_node = prog_node.start("Flush Module", 0); const sub_prog_node = prog_node.start("Flush Module", 0);
defer sub_prog_node.end(); defer sub_prog_node.end();
const comp = self.base.comp; const comp = linker.base.comp;
const spv = &self.object.spv;
const diags = &comp.link_diags; const diags = &comp.link_diags;
const gpa = comp.gpa; const gpa = comp.gpa;
// We need to export the list of error names somewhere so that we can pretty-print them in the // 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 // 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. // nonsemantic instruction. For now, just put it in OpSourceExtension with a special name.
var error_info: std.io.Writer.Allocating = .init(self.object.gpa); var error_info: std.io.Writer.Allocating = .init(linker.module.gpa);
defer error_info.deinit(); defer error_info.deinit();
error_info.writer.writeAll("zig_errors:") catch return error.OutOfMemory; error_info.writer.writeAll("zig_errors:") catch return error.OutOfMemory;
const ip = &self.base.comp.zcu.?.intern_pool; const ip = &linker.base.comp.zcu.?.intern_pool;
for (ip.global_error_set.getNamesFromMainThread()) |name| { for (ip.global_error_set.getNamesFromMainThread()) |name| {
// Errors can contain pretty much any character - to encode them in a string we must escape // 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 // 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. // 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. // We're using : as separator, which is a reserved character.
error_info.writer.writeByte(':') catch return error.OutOfMemory; error_info.writer.writeByte(':') catch return error.OutOfMemory;
std.Uri.Component.percentEncode( std.Uri.Component.percentEncode(
&error_info.writer, &error_info.writer,
@ -228,36 +272,34 @@ pub fn flush(
}.isValidChar, }.isValidChar,
) catch return error.OutOfMemory; ) catch return error.OutOfMemory;
} }
try spv.sections.debug_strings.emit(gpa, .OpSourceExtension, .{ try linker.module.sections.debug_strings.emit(gpa, .OpSourceExtension, .{
.extension = error_info.getWritten(), .extension = error_info.getWritten(),
}); });
const module = try spv.finalize(arena); const module = try linker.module.finalize(arena);
errdefer arena.free(module); errdefer arena.free(module);
const linked_module = self.linkModule(arena, module, sub_prog_node) catch |err| switch (err) { const linked_module = linker.linkModule(arena, module, sub_prog_node) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory, error.OutOfMemory => return error.OutOfMemory,
else => |other| return diags.fail("error while linking: {s}", .{@errorName(other)}), else => |other| return diags.fail("error while linking: {s}", .{@errorName(other)}),
}; };
self.base.file.?.writeAll(std.mem.sliceAsBytes(linked_module)) catch |err| linker.base.file.?.writeAll(std.mem.sliceAsBytes(linked_module)) catch |err|
return diags.fail("failed to write: {s}", .{@errorName(err)}); return diags.fail("failed to write: {s}", .{@errorName(err)});
} }
fn linkModule(self: *SpirV, a: Allocator, module: []Word, progress: std.Progress.Node) ![]Word { fn linkModule(linker: *Linker, arena: Allocator, module: []Word, progress: std.Progress.Node) ![]Word {
_ = self; _ = linker;
const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig"); const lower_invocation_globals = @import("SpirV/lower_invocation_globals.zig");
const prune_unused = @import("SpirV/prune_unused.zig"); const prune_unused = @import("SpirV/prune_unused.zig");
const dedup = @import("SpirV/deduplicate.zig");
var parser = try BinaryModule.Parser.init(a); var parser = try BinaryModule.Parser.init(arena);
defer parser.deinit(); defer parser.deinit();
var binary = try parser.parse(module); var binary = try parser.parse(module);
try lower_invocation_globals.run(&parser, &binary, progress); try lower_invocation_globals.run(&parser, &binary, progress);
try prune_unused.run(&parser, &binary, progress); try prune_unused.run(&parser, &binary, progress);
try dedup.run(&parser, &binary, progress);
return binary.finalize(a); return binary.finalize(arena);
} }

View File

@ -1,553 +0,0 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const log = std.log.scoped(.spirv_link);
const assert = std.debug.assert;
const BinaryModule = @import("BinaryModule.zig");
const Section = @import("../../codegen/spirv/Section.zig");
const spec = @import("../../codegen/spirv/spec.zig");
const Opcode = spec.Opcode;
const ResultId = spec.Id;
const Word = spec.Word;
fn canDeduplicate(opcode: Opcode) bool {
return switch (opcode) {
.OpTypeForwardPointer => false, // Don't need to handle these
.OpGroupDecorate, .OpGroupMemberDecorate => {
// These are deprecated, so don't bother supporting them for now.
return false;
},
// Debug decoration-style instructions
.OpName, .OpMemberName => true,
else => switch (opcode.class()) {
.type_declaration,
.constant_creation,
.annotation,
=> true,
else => false,
},
};
}
const ModuleInfo = struct {
/// This models a type, decoration or constant instruction
/// and its dependencies.
const Entity = struct {
/// The type that this entity represents. This is just
/// the instruction opcode.
kind: Opcode,
/// The offset of this entity's operands, in
/// `binary.instructions`.
first_operand: u32,
/// The number of operands in this entity
num_operands: u16,
/// The (first_operand-relative) offset of the result-id,
/// or the entity that is affected by this entity if this entity
/// is a decoration.
result_id_index: u16,
/// The first decoration in `self.decorations`.
first_decoration: u32,
fn operands(self: Entity, binary: *const BinaryModule) []const Word {
return binary.instructions[self.first_operand..][0..self.num_operands];
}
};
/// Maps result-id to Entity's
entities: std.AutoArrayHashMapUnmanaged(ResultId, Entity),
/// A bit set that keeps track of which operands are result-ids.
/// Note: This also includes any result-id!
/// Because we need these values when recoding the module anyway,
/// it contains the status of ALL operands in the module.
operand_is_id: std.DynamicBitSetUnmanaged,
/// Store of decorations for each entity.
decorations: []const Entity,
pub fn parse(
arena: Allocator,
parser: *BinaryModule.Parser,
binary: BinaryModule,
) !ModuleInfo {
var entities = std.AutoArrayHashMap(ResultId, Entity).init(arena);
var id_offsets = std.ArrayList(u16).init(arena);
var operand_is_id = try std.DynamicBitSetUnmanaged.initEmpty(arena, binary.instructions.len);
var decorations = std.MultiArrayList(struct { target_id: ResultId, entity: Entity }){};
var it = binary.iterateInstructions();
while (it.next()) |inst| {
id_offsets.items.len = 0;
try parser.parseInstructionResultIds(binary, inst, &id_offsets);
const first_operand_offset: u32 = @intCast(inst.offset + 1);
for (id_offsets.items) |offset| {
operand_is_id.set(first_operand_offset + offset);
}
if (!canDeduplicate(inst.opcode)) continue;
const result_id_index: u16 = switch (inst.opcode.class()) {
.type_declaration, .annotation, .debug => 0,
.constant_creation => 1,
else => unreachable,
};
const result_id: ResultId = @enumFromInt(inst.operands[id_offsets.items[result_id_index]]);
const entity = Entity{
.kind = inst.opcode,
.first_operand = first_operand_offset,
.num_operands = @intCast(inst.operands.len),
.result_id_index = result_id_index,
.first_decoration = undefined, // Filled in later
};
switch (inst.opcode.class()) {
.annotation, .debug => {
try decorations.append(arena, .{
.target_id = result_id,
.entity = entity,
});
},
.type_declaration, .constant_creation => {
const entry = try entities.getOrPut(result_id);
if (entry.found_existing) {
log.err("type or constant {f} has duplicate definition", .{result_id});
return error.DuplicateId;
}
entry.value_ptr.* = entity;
},
else => unreachable,
}
}
// Sort decorations by the index of the result-id in `entities.
// This ensures not only that the decorations of a particular reuslt-id
// are continuous, but the subsequences also appear in the same order as in `entities`.
const SortContext = struct {
entities: std.AutoArrayHashMapUnmanaged(ResultId, Entity),
ids: []const ResultId,
pub fn lessThan(ctx: @This(), a_index: usize, b_index: usize) bool {
// If any index is not in the entities set, its because its not a
// deduplicatable result-id. Those should be considered largest and
// float to the end.
const entity_index_a = ctx.entities.getIndex(ctx.ids[a_index]) orelse return false;
const entity_index_b = ctx.entities.getIndex(ctx.ids[b_index]) orelse return true;
return entity_index_a < entity_index_b;
}
};
decorations.sort(SortContext{
.entities = entities.unmanaged,
.ids = decorations.items(.target_id),
});
// Now go through the decorations and add the offsets to the entities list.
var decoration_i: u32 = 0;
const target_ids = decorations.items(.target_id);
for (entities.keys(), entities.values()) |id, *entity| {
entity.first_decoration = decoration_i;
// Scan ahead to the next decoration
while (decoration_i < target_ids.len and target_ids[decoration_i] == id) {
decoration_i += 1;
}
}
return .{
.entities = entities.unmanaged,
.operand_is_id = operand_is_id,
// There may be unrelated decorations at the end, so make sure to
// slice those off.
.decorations = decorations.items(.entity)[0..decoration_i],
};
}
fn entityDecorationsByIndex(self: ModuleInfo, index: usize) []const Entity {
const values = self.entities.values();
const first_decoration = values[index].first_decoration;
if (index == values.len - 1) {
return self.decorations[first_decoration..];
} else {
const next_first_decoration = values[index + 1].first_decoration;
return self.decorations[first_decoration..next_first_decoration];
}
}
};
const EntityContext = struct {
a: Allocator,
ptr_map_a: std.AutoArrayHashMapUnmanaged(ResultId, void) = .empty,
ptr_map_b: std.AutoArrayHashMapUnmanaged(ResultId, void) = .empty,
info: *const ModuleInfo,
binary: *const BinaryModule,
fn deinit(self: *EntityContext) void {
self.ptr_map_a.deinit(self.a);
self.ptr_map_b.deinit(self.a);
self.* = undefined;
}
fn equalizeMapCapacity(self: *EntityContext) !void {
const cap = @max(self.ptr_map_a.capacity(), self.ptr_map_b.capacity());
try self.ptr_map_a.ensureTotalCapacity(self.a, cap);
try self.ptr_map_b.ensureTotalCapacity(self.a, cap);
}
fn hash(self: *EntityContext, id: ResultId) !u64 {
var hasher = std.hash.Wyhash.init(0);
self.ptr_map_a.clearRetainingCapacity();
try self.hashInner(&hasher, id);
return hasher.final();
}
fn hashInner(self: *EntityContext, hasher: *std.hash.Wyhash, id: ResultId) error{OutOfMemory}!void {
const index = self.info.entities.getIndex(id) orelse {
// Index unknown, the type or constant may depend on another result-id
// that couldn't be deduplicated and so it wasn't added to info.entities.
// In this case, just has the ID itself.
std.hash.autoHash(hasher, id);
return;
};
const entity = self.info.entities.values()[index];
// If the current pointer is recursive, don't immediately add it to the map. This is to ensure that
// if the current pointer is already recursive, it gets the same hash a pointer that points to the
// same child but has a different result-id.
if (entity.kind == .OpTypePointer) {
// This may be either a pointer that is forward-referenced in the future,
// or a forward reference to a pointer.
// Note: We use the **struct** here instead of the pointer itself, to avoid an edge case like this:
//
// A - C*'
// \
// C - C*'
// /
// B - C*"
//
// In this case, hashing A goes like
// A -> C*' -> C -> C*' recursion
// And hashing B goes like
// B -> C*" -> C -> C*' -> C -> C*' recursion
// The are several calls to ptrType in codegen that may C*' and C*" to be generated as separate
// types. This is not a problem for C itself though - this can only be generated through resolveType()
// and so ensures equality by Zig's type system. Technically the above problem is still present, but it
// would only be present in a structure such as
//
// A - C*' - C'
// \
// C*" - C - C*
// /
// B
//
// where there is a duplicate definition of struct C. Resolving this requires a much more time consuming
// algorithm though, and because we don't expect any correctness issues with it, we leave that for now.
// TODO: Do we need to mind the storage class here? Its going to be recursive regardless, right?
const struct_id: ResultId = @enumFromInt(entity.operands(self.binary)[2]);
const entry = try self.ptr_map_a.getOrPut(self.a, struct_id);
if (entry.found_existing) {
// Pointer already seen. Hash the index instead of recursing into its children.
std.hash.autoHash(hasher, entry.index);
return;
}
}
try self.hashEntity(hasher, entity);
// Process decorations.
const decorations = self.info.entityDecorationsByIndex(index);
for (decorations) |decoration| {
try self.hashEntity(hasher, decoration);
}
if (entity.kind == .OpTypePointer) {
const struct_id: ResultId = @enumFromInt(entity.operands(self.binary)[2]);
assert(self.ptr_map_a.swapRemove(struct_id));
}
}
fn hashEntity(self: *EntityContext, hasher: *std.hash.Wyhash, entity: ModuleInfo.Entity) !void {
std.hash.autoHash(hasher, entity.kind);
// Process operands
const operands = entity.operands(self.binary);
for (operands, 0..) |operand, i| {
if (i == entity.result_id_index) {
// Not relevant, skip...
continue;
} else if (self.info.operand_is_id.isSet(entity.first_operand + i)) {
// Operand is ID
try self.hashInner(hasher, @enumFromInt(operand));
} else {
// Operand is merely data
std.hash.autoHash(hasher, operand);
}
}
}
fn eql(self: *EntityContext, a: ResultId, b: ResultId) !bool {
self.ptr_map_a.clearRetainingCapacity();
self.ptr_map_b.clearRetainingCapacity();
return try self.eqlInner(a, b);
}
fn eqlInner(self: *EntityContext, id_a: ResultId, id_b: ResultId) error{OutOfMemory}!bool {
const maybe_index_a = self.info.entities.getIndex(id_a);
const maybe_index_b = self.info.entities.getIndex(id_b);
if (maybe_index_a == null and maybe_index_b == null) {
// Both indices unknown. In this case the type or constant
// may depend on another result-id that couldn't be deduplicated
// (so it wasn't added to info.entities). In this case, that particular
// result-id should be the same one.
return id_a == id_b;
}
const index_a = maybe_index_a orelse return false;
const index_b = maybe_index_b orelse return false;
const entity_a = self.info.entities.values()[index_a];
const entity_b = self.info.entities.values()[index_b];
if (entity_a.kind != entity_b.kind) {
return false;
}
if (entity_a.kind == .OpTypePointer) {
// May be a forward reference, or should be saved as a potential
// forward reference in the future. Whatever the case, it should
// be the same for both a and b.
const struct_id_a: ResultId = @enumFromInt(entity_a.operands(self.binary)[2]);
const struct_id_b: ResultId = @enumFromInt(entity_b.operands(self.binary)[2]);
const entry_a = try self.ptr_map_a.getOrPut(self.a, struct_id_a);
const entry_b = try self.ptr_map_b.getOrPut(self.a, struct_id_b);
if (entry_a.found_existing != entry_b.found_existing) return false;
if (entry_a.index != entry_b.index) return false;
if (entry_a.found_existing) {
// No need to recurse.
return true;
}
}
if (!try self.eqlEntities(entity_a, entity_b)) {
return false;
}
// Compare decorations.
const decorations_a = self.info.entityDecorationsByIndex(index_a);
const decorations_b = self.info.entityDecorationsByIndex(index_b);
if (decorations_a.len != decorations_b.len) {
return false;
}
for (decorations_a, decorations_b) |decoration_a, decoration_b| {
if (!try self.eqlEntities(decoration_a, decoration_b)) {
return false;
}
}
if (entity_a.kind == .OpTypePointer) {
const struct_id_a: ResultId = @enumFromInt(entity_a.operands(self.binary)[2]);
const struct_id_b: ResultId = @enumFromInt(entity_b.operands(self.binary)[2]);
assert(self.ptr_map_a.swapRemove(struct_id_a));
assert(self.ptr_map_b.swapRemove(struct_id_b));
}
return true;
}
fn eqlEntities(self: *EntityContext, entity_a: ModuleInfo.Entity, entity_b: ModuleInfo.Entity) !bool {
if (entity_a.kind != entity_b.kind) {
return false;
} else if (entity_a.result_id_index != entity_a.result_id_index) {
return false;
}
const operands_a = entity_a.operands(self.binary);
const operands_b = entity_b.operands(self.binary);
// Note: returns false for operands that have explicit defaults in optional operands... oh well
if (operands_a.len != operands_b.len) {
return false;
}
for (operands_a, operands_b, 0..) |operand_a, operand_b, i| {
const a_is_id = self.info.operand_is_id.isSet(entity_a.first_operand + i);
const b_is_id = self.info.operand_is_id.isSet(entity_b.first_operand + i);
if (a_is_id != b_is_id) {
return false;
} else if (i == entity_a.result_id_index) {
// result-id for both...
continue;
} else if (a_is_id) {
// Both are IDs, so recurse.
if (!try self.eqlInner(@enumFromInt(operand_a), @enumFromInt(operand_b))) {
return false;
}
} else if (operand_a != operand_b) {
return false;
}
}
return true;
}
};
/// This struct is a wrapper around EntityContext that adapts it for
/// use in a hash map. Because EntityContext allocates, it cannot be
/// used. This wrapper simply assumes that the maps have been allocated
/// the max amount of memory they are going to use.
/// This is done by pre-hashing all keys.
const EntityHashContext = struct {
entity_context: *EntityContext,
pub fn hash(self: EntityHashContext, key: ResultId) u64 {
return self.entity_context.hash(key) catch unreachable;
}
pub fn eql(self: EntityHashContext, a: ResultId, b: ResultId) bool {
return self.entity_context.eql(a, b) catch unreachable;
}
};
pub fn run(parser: *BinaryModule.Parser, binary: *BinaryModule, progress: std.Progress.Node) !void {
const sub_node = progress.start("deduplicate", 0);
defer sub_node.end();
var arena = std.heap.ArenaAllocator.init(parser.a);
defer arena.deinit();
const a = arena.allocator();
const info = try ModuleInfo.parse(a, parser, binary.*);
// Hash all keys once so that the maps can be allocated the right size.
var ctx = EntityContext{
.a = a,
.info = &info,
.binary = binary,
};
for (info.entities.keys()) |id| {
_ = try ctx.hash(id);
}
// hash only uses ptr_map_a, so allocate ptr_map_b too
try ctx.equalizeMapCapacity();
// Figure out which entities can be deduplicated.
var map = std.HashMap(ResultId, void, EntityHashContext, 80).initContext(a, .{
.entity_context = &ctx,
});
var replace = std.AutoArrayHashMap(ResultId, ResultId).init(a);
for (info.entities.keys()) |id| {
const entry = try map.getOrPut(id);
if (entry.found_existing) {
try replace.putNoClobber(id, entry.key_ptr.*);
}
}
sub_node.setEstimatedTotalItems(binary.instructions.len);
// Now process the module, and replace instructions where needed.
var section = Section{};
var it = binary.iterateInstructions();
var new_functions_section: ?usize = null;
var new_operands = std.ArrayList(u32).init(a);
var emitted_ptrs = std.AutoHashMap(ResultId, void).init(a);
while (it.next()) |inst| {
defer sub_node.setCompletedItems(inst.offset);
// Result-id can only be the first or second operand
const inst_spec = parser.getInstSpec(inst.opcode).?;
const maybe_result_id_offset: ?u16 = for (0..2) |i| {
if (inst_spec.operands.len > i and inst_spec.operands[i].kind == .id_result) {
break @intCast(i);
}
} else null;
if (maybe_result_id_offset) |offset| {
const result_id: ResultId = @enumFromInt(inst.operands[offset]);
if (replace.contains(result_id)) continue;
}
switch (inst.opcode) {
.OpFunction => if (new_functions_section == null) {
new_functions_section = section.instructions.items.len;
},
.OpTypeForwardPointer => continue, // We re-emit these where needed
else => {},
}
switch (inst.opcode.class()) {
.annotation, .debug => {
// For decoration-style instructions, only emit them
// if the target is not removed.
const target: ResultId = @enumFromInt(inst.operands[0]);
if (replace.contains(target)) continue;
},
else => {},
}
// Re-emit the instruction, but replace all the IDs.
new_operands.items.len = 0;
try new_operands.appendSlice(inst.operands);
for (new_operands.items, 0..) |*operand, i| {
const is_id = info.operand_is_id.isSet(inst.offset + 1 + i);
if (!is_id) continue;
if (replace.get(@enumFromInt(operand.*))) |new_id| {
operand.* = @intFromEnum(new_id);
}
if (maybe_result_id_offset == null or maybe_result_id_offset.? != i) {
// Only emit forward pointers before type, constant, and global instructions.
// Debug and Annotation instructions don't need the forward pointer, and it
// messes up the logical layout of the module.
switch (inst.opcode.class()) {
.type_declaration, .constant_creation, .memory => {},
else => continue,
}
const id: ResultId = @enumFromInt(operand.*);
const index = info.entities.getIndex(id) orelse continue;
const entity = info.entities.values()[index];
if (entity.kind == .OpTypePointer and !emitted_ptrs.contains(id)) {
// Grab the pointer's storage class from its operands in the original
// module.
const storage_class: spec.StorageClass = @enumFromInt(entity.operands(binary)[1]);
try section.emit(a, .OpTypeForwardPointer, .{
.pointer_type = id,
.storage_class = storage_class,
});
try emitted_ptrs.put(id, {});
}
}
}
if (inst.opcode == .OpTypePointer) {
const result_id: ResultId = @enumFromInt(new_operands.items[maybe_result_id_offset.?]);
try emitted_ptrs.put(result_id, {});
}
try section.emitRawInstruction(a, inst.opcode, new_operands.items);
}
for (replace.keys()) |key| {
_ = binary.ext_inst_map.remove(key);
_ = binary.arith_type_width.remove(key);
}
binary.instructions = try parser.a.dupe(Word, section.toWords());
binary.sections.functions = new_functions_section orelse binary.instructions.len;
}

View File

@ -140,6 +140,7 @@ test "packed union initialized with a runtime value" {
if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO 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_sparc64) return error.SkipZigTest; // TODO
if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_riscv64) return error.SkipZigTest;
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
const Fields = packed struct { const Fields = packed struct {
timestamp: u50, timestamp: u50,

View File

@ -1036,6 +1036,8 @@ test "sentinel-terminated 0-length slices" {
} }
test "peer slices keep abi alignment with empty struct" { test "peer slices keep abi alignment with empty struct" {
if (builtin.zig_backend == .stage2_spirv) return error.SkipZigTest;
var cond: bool = undefined; var cond: bool = undefined;
cond = false; cond = false;
const slice = if (cond) &[1]u32{42} else &.{}; const slice = if (cond) &[1]u32{42} else &.{};

View File

@ -12,6 +12,7 @@ const ExtendedStructSet = std.StringHashMap(void);
const Extension = struct { const Extension = struct {
name: []const u8, name: []const u8,
opcode_name: []const u8,
spec: ExtensionRegistry, spec: ExtensionRegistry,
}; };
@ -44,23 +45,11 @@ const OperandKindMap = std.ArrayHashMap(StringPair, OperandKind, StringPairConte
/// Khronos made it so that these names are not defined explicitly, so /// Khronos made it so that these names are not defined explicitly, so
/// we need to hardcode it (like they did). /// we need to hardcode it (like they did).
/// See https://github.com/KhronosGroup/SPIRV-Registry/ /// See https://github.com/KhronosGroup/SPIRV-Registry
const set_names = std.StaticStringMap([]const u8).initComptime(.{ const set_names = std.StaticStringMap(struct { []const u8, []const u8 }).initComptime(.{
.{ "opencl.std.100", "OpenCL.std" }, .{ "opencl.std.100", .{ "OpenCL.std", "OpenClOpcode" } },
.{ "glsl.std.450", "GLSL.std.450" }, .{ "glsl.std.450", .{ "GLSL.std.450", "GlslOpcode" } },
.{ "opencl.debuginfo.100", "OpenCL.DebugInfo.100" }, .{ "zig", .{ "zig", "Zig" } },
.{ "spv-amd-shader-ballot", "SPV_AMD_shader_ballot" },
.{ "nonsemantic.shader.debuginfo.100", "NonSemantic.Shader.DebugInfo.100" },
.{ "nonsemantic.vkspreflection", "NonSemantic.VkspReflection" },
.{ "nonsemantic.clspvreflection", "NonSemantic.ClspvReflection.6" }, // This version needs to be handled manually
.{ "spv-amd-gcn-shader", "SPV_AMD_gcn_shader" },
.{ "spv-amd-shader-trinary-minmax", "SPV_AMD_shader_trinary_minmax" },
.{ "debuginfo", "DebugInfo" },
.{ "nonsemantic.debugprintf", "NonSemantic.DebugPrintf" },
.{ "spv-amd-shader-explicit-vertex-parameter", "SPV_AMD_shader_explicit_vertex_parameter" },
.{ "nonsemantic.debugbreak", "NonSemantic.DebugBreak" },
.{ "tosa.001000.1", "SPV_EXT_INST_TYPE_TOSA_001000_1" },
.{ "zig", "zig" },
}); });
var arena = std.heap.ArenaAllocator.init(std.heap.smp_allocator); var arena = std.heap.ArenaAllocator.init(std.heap.smp_allocator);
@ -78,7 +67,7 @@ pub fn main() !void {
const dir = try std.fs.cwd().openDir(json_path, .{ .iterate = true }); const dir = try std.fs.cwd().openDir(json_path, .{ .iterate = true });
const core_spec = try readRegistry(CoreRegistry, dir, "spirv.core.grammar.json"); const core_spec = try readRegistry(CoreRegistry, dir, "spirv.core.grammar.json");
std.sort.block(Instruction, core_spec.instructions, CmpInst{}, CmpInst.lt); std.mem.sortUnstable(Instruction, core_spec.instructions, CmpInst{}, CmpInst.lt);
var exts = std.ArrayList(Extension).init(allocator); var exts = std.ArrayList(Extension).init(allocator);
@ -134,14 +123,24 @@ fn readExtRegistry(exts: *std.ArrayList(Extension), dir: std.fs.Dir, sub_path: [
const name = filename["extinst.".len .. filename.len - ".grammar.json".len]; const name = filename["extinst.".len .. filename.len - ".grammar.json".len];
const spec = try readRegistry(ExtensionRegistry, dir, sub_path); const spec = try readRegistry(ExtensionRegistry, dir, sub_path);
const set_name = set_names.get(name) orelse {
std.log.info("ignored instruction set '{s}'", .{name});
return;
};
std.sort.block(Instruction, spec.instructions, CmpInst{}, CmpInst.lt); std.sort.block(Instruction, spec.instructions, CmpInst{}, CmpInst.lt);
try exts.append(.{ .name = set_names.get(name).?, .spec = spec }); try exts.append(.{
.name = set_name.@"0",
.opcode_name = set_name.@"1",
.spec = spec,
});
} }
fn readRegistry(comptime RegistryType: type, dir: std.fs.Dir, path: []const u8) !RegistryType { fn readRegistry(comptime RegistryType: type, dir: std.fs.Dir, path: []const u8) !RegistryType {
const spec = try dir.readFileAlloc(allocator, path, std.math.maxInt(usize)); const spec = try dir.readFileAlloc(allocator, path, std.math.maxInt(usize));
// Required for json parsing. // Required for json parsing.
// TODO: ALI
@setEvalBranchQuota(10000); @setEvalBranchQuota(10000);
var scanner = std.json.Scanner.initCompleteInput(allocator, spec); var scanner = std.json.Scanner.initCompleteInput(allocator, spec);
@ -191,7 +190,11 @@ fn tagPriorityScore(tag: []const u8) usize {
} }
} }
fn render(writer: *std.io.Writer, registry: CoreRegistry, extensions: []const Extension) !void { fn render(
writer: *std.io.Writer,
registry: CoreRegistry,
extensions: []const Extension,
) !void {
try writer.writeAll( try writer.writeAll(
\\//! This file is auto-generated by tools/gen_spirv_spec.zig. \\//! This file is auto-generated by tools/gen_spirv_spec.zig.
\\ \\
@ -221,6 +224,16 @@ fn render(writer: *std.io.Writer, registry: CoreRegistry, extensions: []const Ex
\\ } \\ }
\\}; \\};
\\ \\
\\pub const IdRange = struct {
\\ base: u32,
\\ len: u32,
\\
\\ pub fn at(range: IdRange, i: usize) Id {
\\ std.debug.assert(i < range.len);
\\ return @enumFromInt(range.base + i);
\\ }
\\};
\\
\\pub const LiteralInteger = Word; \\pub const LiteralInteger = Word;
\\pub const LiteralFloat = Word; \\pub const LiteralFloat = Word;
\\pub const LiteralString = []const u8; \\pub const LiteralString = []const u8;
@ -307,13 +320,18 @@ fn render(writer: *std.io.Writer, registry: CoreRegistry, extensions: []const Ex
// Note: extensions don't seem to have class. // Note: extensions don't seem to have class.
try renderClass(writer, registry.instructions); try renderClass(writer, registry.instructions);
try renderOperandKind(writer, all_operand_kinds.values()); try renderOperandKind(writer, all_operand_kinds.values());
try renderOpcodes(writer, registry.instructions, extended_structs);
try renderOpcodes(writer, "Opcode", true, registry.instructions, extended_structs);
for (extensions) |ext| {
try renderOpcodes(writer, ext.opcode_name, false, ext.spec.instructions, extended_structs);
}
try renderOperandKinds(writer, all_operand_kinds.values(), extended_structs); try renderOperandKinds(writer, all_operand_kinds.values(), extended_structs);
try renderInstructionSet(writer, registry, extensions, all_operand_kinds); try renderInstructionSet(writer, registry, extensions, all_operand_kinds);
} }
fn renderInstructionSet( fn renderInstructionSet(
writer: anytype, writer: *std.io.Writer,
core: CoreRegistry, core: CoreRegistry,
extensions: []const Extension, extensions: []const Extension,
all_operand_kinds: OperandKindMap, all_operand_kinds: OperandKindMap,
@ -324,7 +342,7 @@ fn renderInstructionSet(
); );
for (extensions) |ext| { for (extensions) |ext| {
try writer.print("{f},\n", .{formatId(ext.name)}); try writer.print("{f},\n", .{std.zig.fmtId(ext.name)});
} }
try writer.writeAll( try writer.writeAll(
@ -348,7 +366,7 @@ fn renderInstructionSet(
} }
fn renderInstructionsCase( fn renderInstructionsCase(
writer: anytype, writer: *std.io.Writer,
set_name: []const u8, set_name: []const u8,
instructions: []const Instruction, instructions: []const Instruction,
all_operand_kinds: OperandKindMap, all_operand_kinds: OperandKindMap,
@ -357,7 +375,7 @@ fn renderInstructionsCase(
// but there aren't so many total aliases and that would add more overhead in total. We will // but there aren't so many total aliases and that would add more overhead in total. We will
// just filter those out when needed. // just filter those out when needed.
try writer.print(".{f} => &.{{\n", .{formatId(set_name)}); try writer.print(".{f} => &.{{\n", .{std.zig.fmtId(set_name)});
for (instructions) |inst| { for (instructions) |inst| {
try writer.print( try writer.print(
@ -395,7 +413,7 @@ fn renderInstructionsCase(
); );
} }
fn renderClass(writer: anytype, instructions: []const Instruction) !void { fn renderClass(writer: *std.io.Writer, instructions: []const Instruction) !void {
var class_map = std.StringArrayHashMap(void).init(allocator); var class_map = std.StringArrayHashMap(void).init(allocator);
for (instructions) |inst| { for (instructions) |inst| {
@ -444,7 +462,7 @@ fn formatId(identifier: []const u8) std.fmt.Alt(Formatter, Formatter.format) {
return .{ .data = .{ .data = identifier } }; return .{ .data = .{ .data = identifier } };
} }
fn renderOperandKind(writer: anytype, operands: []const OperandKind) !void { fn renderOperandKind(writer: *std.io.Writer, operands: []const OperandKind) !void {
try writer.writeAll( try writer.writeAll(
\\pub const OperandKind = enum { \\pub const OperandKind = enum {
\\ opcode, \\ opcode,
@ -500,7 +518,7 @@ fn renderOperandKind(writer: anytype, operands: []const OperandKind) !void {
try writer.writeAll("};\n}\n};\n"); try writer.writeAll("};\n}\n};\n");
} }
fn renderEnumerant(writer: anytype, enumerant: Enumerant) !void { fn renderEnumerant(writer: *std.io.Writer, enumerant: Enumerant) !void {
try writer.print(".{{.name = \"{s}\", .value = ", .{enumerant.enumerant}); try writer.print(".{{.name = \"{s}\", .value = ", .{enumerant.enumerant});
switch (enumerant.value) { switch (enumerant.value) {
.bitflag => |flag| try writer.writeAll(flag), .bitflag => |flag| try writer.writeAll(flag),
@ -517,7 +535,9 @@ fn renderEnumerant(writer: anytype, enumerant: Enumerant) !void {
} }
fn renderOpcodes( fn renderOpcodes(
writer: anytype, writer: *std.io.Writer,
opcode_type_name: []const u8,
want_operands: bool,
instructions: []const Instruction, instructions: []const Instruction,
extended_structs: ExtendedStructSet, extended_structs: ExtendedStructSet,
) !void { ) !void {
@ -528,7 +548,9 @@ fn renderOpcodes(
try aliases.ensureTotalCapacity(instructions.len); try aliases.ensureTotalCapacity(instructions.len);
for (instructions, 0..) |inst, i| { for (instructions, 0..) |inst, i| {
if (std.mem.eql(u8, inst.class.?, "@exclude")) continue; if (inst.class) |class| {
if (std.mem.eql(u8, class, "@exclude")) continue;
}
const result = inst_map.getOrPutAssumeCapacity(inst.opcode); const result = inst_map.getOrPutAssumeCapacity(inst.opcode);
if (!result.found_existing) { if (!result.found_existing) {
@ -552,58 +574,67 @@ fn renderOpcodes(
const instructions_indices = inst_map.values(); const instructions_indices = inst_map.values();
try writer.writeAll("pub const Opcode = enum(u16) {\n"); try writer.print("\npub const {f} = enum(u16) {{\n", .{std.zig.fmtId(opcode_type_name)});
for (instructions_indices) |i| { for (instructions_indices) |i| {
const inst = instructions[i]; const inst = instructions[i];
try writer.print("{f} = {},\n", .{ std.zig.fmtId(inst.opname), inst.opcode }); try writer.print("{f} = {},\n", .{ std.zig.fmtId(inst.opname), inst.opcode });
} }
try writer.writeAll( try writer.writeAll("\n");
\\
);
for (aliases.items) |alias| { for (aliases.items) |alias| {
try writer.print("pub const {f} = Opcode.{f};\n", .{ try writer.print("pub const {f} = {f}.{f};\n", .{
formatId(instructions[alias.inst].opname), formatId(instructions[alias.inst].opname),
std.zig.fmtId(opcode_type_name),
formatId(instructions[alias.alias].opname), formatId(instructions[alias.alias].opname),
}); });
} }
try writer.writeAll( if (want_operands) {
\\ try writer.print(
\\pub fn Operands(comptime self: Opcode) type { \\
\\ return switch (self) { \\pub fn Operands(comptime self: {f}) type {{
\\ \\ return switch (self) {{
); \\
, .{std.zig.fmtId(opcode_type_name)});
for (instructions_indices) |i| { for (instructions_indices) |i| {
const inst = instructions[i]; const inst = instructions[i];
try renderOperand(writer, .instruction, inst.opname, inst.operands, extended_structs, false); try renderOperand(writer, .instruction, inst.opname, inst.operands, extended_structs, false);
}
try writer.writeAll(
\\ };
\\}
\\
);
try writer.print(
\\pub fn class(self: {f}) Class {{
\\ return switch (self) {{
\\
, .{std.zig.fmtId(opcode_type_name)});
for (instructions_indices) |i| {
const inst = instructions[i];
try writer.print(".{f} => .{f},\n", .{ std.zig.fmtId(inst.opname), formatId(inst.class.?) });
}
try writer.writeAll(
\\ };
\\}
\\
);
} }
try writer.writeAll( try writer.writeAll(
\\ };
\\}
\\pub fn class(self: Opcode) Class {
\\ return switch (self) {
\\
);
for (instructions_indices) |i| {
const inst = instructions[i];
try writer.print(".{f} => .{f},\n", .{ std.zig.fmtId(inst.opname), formatId(inst.class.?) });
}
try writer.writeAll(
\\ };
\\}
\\}; \\};
\\ \\
); );
} }
fn renderOperandKinds( fn renderOperandKinds(
writer: anytype, writer: *std.io.Writer,
kinds: []const OperandKind, kinds: []const OperandKind,
extended_structs: ExtendedStructSet, extended_structs: ExtendedStructSet,
) !void { ) !void {
@ -617,7 +648,7 @@ fn renderOperandKinds(
} }
fn renderValueEnum( fn renderValueEnum(
writer: anytype, writer: *std.io.Writer,
enumeration: OperandKind, enumeration: OperandKind,
extended_structs: ExtendedStructSet, extended_structs: ExtendedStructSet,
) !void { ) !void {
@ -695,7 +726,7 @@ fn renderValueEnum(
} }
fn renderBitEnum( fn renderBitEnum(
writer: anytype, writer: *std.io.Writer,
enumeration: OperandKind, enumeration: OperandKind,
extended_structs: ExtendedStructSet, extended_structs: ExtendedStructSet,
) !void { ) !void {
@ -778,7 +809,7 @@ fn renderBitEnum(
} }
fn renderOperand( fn renderOperand(
writer: anytype, writer: *std.io.Writer,
kind: enum { kind: enum {
@"union", @"union",
instruction, instruction,
@ -862,7 +893,7 @@ fn renderOperand(
try writer.writeAll(",\n"); try writer.writeAll(",\n");
} }
fn renderFieldName(writer: anytype, operands: []const Operand, field_index: usize) !void { fn renderFieldName(writer: *std.io.Writer, operands: []const Operand, field_index: usize) !void {
const operand = operands[field_index]; const operand = operands[field_index];
derive_from_kind: { derive_from_kind: {