SPIR-V: Debug line info/source info

This commit is contained in:
Robin Voetter 2021-05-21 02:08:14 +02:00
parent e3be1a1e88
commit 6634abfd26
2 changed files with 122 additions and 44 deletions

View File

@ -40,34 +40,92 @@ pub fn writeInstruction(code: *std.ArrayList(Word), opcode: Opcode, args: []cons
try code.appendSlice(args);
}
pub fn writeInstructionWithString(code: *std.ArrayList(Word), opcode: Opcode, args: []const Word, str: []const u8) !void {
// Str needs to be written zero-terminated, so we need to add one to the length.
const zero_terminated_len = str.len + 1;
const str_words = (zero_terminated_len + @sizeOf(Word) - 1) / @sizeOf(Word);
try writeOpcode(code, opcode, @intCast(u16, args.len + str_words));
try code.ensureUnusedCapacity(args.len + str_words);
code.appendSliceAssumeCapacity(args);
// TODO: Not actually sure whether this is correct for big-endian.
// See https://www.khronos.org/registry/spir-v/specs/unified1/SPIRV.html#Literal
var i: usize = 0;
while (i < zero_terminated_len) : (i += @sizeOf(Word)) {
var word: Word = 0;
var j: usize = 0;
while (j < @sizeOf(Word) and i + j < str.len) : (j += 1) {
word |= @as(Word, str[i + j]) << @intCast(std.math.Log2Int(Word), j * std.meta.bitCount(u8));
}
code.appendAssumeCapacity(word);
}
}
/// This structure represents a SPIR-V (binary) module being compiled, and keeps track of all relevant information.
/// That includes the actual instructions, the current result-id bound, and data structures for querying result-id's
/// of data which needs to be persistent over different calls to Decl code generation.
pub const SPIRVModule = struct {
/// A general-purpose allocator which may be used to allocate temporary resources required for compilation.
gpa: *Allocator,
/// The parent module.
module: *Module,
/// SPIR-V instructions return result-ids. This variable holds the module-wide counter for these.
next_result_id: ResultId,
/// Code of the actual SPIR-V binary, divided into the relevant logical sections.
/// Note: To save some bytes, these could also be unmanaged, but since there is only one instance of SPIRVModule
/// and this removes some clutter in the rest of the backend, it's fine like this.
binary: struct {
/// OpCapability and OpExtension instructions (in that order).
capabilities_and_extensions: std.ArrayList(Word),
/// OpString, OpSourceExtension, OpSource, OpSourceContinued.
debug_strings: std.ArrayList(Word),
/// Type declaration instructions, constant instructions, global variable declarations, OpUndef instructions.
types_globals_constants: std.ArrayList(Word),
/// Regular functions.
fn_decls: std.ArrayList(Word),
},
/// Global type cache to reduce the amount of generated types.
types: TypeMap,
pub fn init(gpa: *Allocator) SPIRVModule {
/// Cache for results of OpString instructions for module file names fed to OpSource.
/// Since OpString is pretty much only used for those, we don't need to keep track of all strings,
/// just the ones for OpLine. Note that OpLine needs the result of OpString, and not that of OpSource.
file_names: std.StringHashMap(ResultId),
pub fn init(gpa: *Allocator, module: *Module) SPIRVModule {
return .{
.gpa = gpa,
.module = module,
.next_result_id = 1, // 0 is an invalid SPIR-V result ID.
.binary = .{
.capabilities_and_extensions = std.ArrayList(Word).init(gpa),
.debug_strings = std.ArrayList(Word).init(gpa),
.types_globals_constants = std.ArrayList(Word).init(gpa),
.fn_decls = std.ArrayList(Word).init(gpa),
},
.types = TypeMap.init(gpa),
.file_names = std.StringHashMap(ResultId).init(gpa),
};
}
pub fn deinit(self: *SPIRVModule) void {
self.binary.types_globals_constants.deinit();
self.binary.fn_decls.deinit();
self.file_names.deinit();
self.types.deinit();
self.binary.fn_decls.deinit();
self.binary.types_globals_constants.deinit();
self.binary.debug_strings.deinit();
self.binary.capabilities_and_extensions.deinit();
}
pub fn allocResultId(self: *SPIRVModule) Word {
@ -78,13 +136,26 @@ pub const SPIRVModule = struct {
pub fn resultIdBound(self: *SPIRVModule) Word {
return self.next_result_id;
}
fn resolveSourceFileName(self: *SPIRVModule, decl: *Decl) !ResultId {
const path = decl.namespace.file_scope.sub_file_path;
const result = try self.file_names.getOrPut(path);
if (!result.found_existing) {
result.entry.value = self.allocResultId();
try writeInstructionWithString(&self.binary.debug_strings, .OpString, &[_]Word{result.entry.value}, path);
try writeInstruction(&self.binary.debug_strings, .OpSource, &[_]Word{
@enumToInt(spec.SourceLanguage.Unknown), // TODO: Register Zig source language.
0, // TODO: Zig version as u32?
result.entry.value,
});
}
return result.entry.value;
}
};
/// This structure is used to compile a declaration, and contains all relevant meta-information to deal with that.
pub const DeclGen = struct {
/// The parent module.
module: *Module,
/// The SPIR-V module code should be put in.
spv: *SPIRVModule,
@ -158,9 +229,8 @@ pub const DeclGen = struct {
};
/// Initialize the common resources of a DeclGen. Some fields are left uninitialized, only set when `gen` is called.
pub fn init(gpa: *Allocator, module: *Module, spv: *SPIRVModule) DeclGen {
pub fn init(gpa: *Allocator, spv: *SPIRVModule) DeclGen {
return .{
.module = module,
.spv = spv,
.args = std.ArrayList(ResultId).init(gpa),
.next_arg_index = undefined,
@ -196,10 +266,14 @@ pub const DeclGen = struct {
self.blocks.deinit();
}
fn getTarget(self: *DeclGen) std.Target {
return self.spv.module.getTarget();
}
fn fail(self: *DeclGen, src: LazySrcLoc, comptime format: []const u8, args: anytype) Error {
@setCold(true);
const src_loc = src.toSrcLocWithDecl(self.decl);
self.error_msg = try Module.ErrorMsg.create(self.module.gpa, src_loc, format, args);
self.error_msg = try Module.ErrorMsg.create(self.spv.module.gpa, src_loc, format, args);
return error.AnalysisFail;
}
@ -227,7 +301,7 @@ pub const DeclGen = struct {
/// TODO: This probably needs an ABI-version as well (especially in combination with SPV_INTEL_arbitrary_precision_integers).
/// TODO: Should the result of this function be cached?
fn backingIntBits(self: *DeclGen, bits: u16) ?u16 {
const target = self.module.getTarget();
const target = self.getTarget();
// The backend will never be asked to compiler a 0-bit integer, so we won't have to handle those in this function.
std.debug.assert(bits != 0);
@ -262,7 +336,7 @@ pub const DeclGen = struct {
/// is no way of knowing whether those are actually supported.
/// TODO: Maybe this should be cached?
fn largestSupportedIntBits(self: *DeclGen) u16 {
const target = self.module.getTarget();
const target = self.getTarget();
return if (Target.spirv.featureSetHas(target.cpu.features, .Int64))
64
else
@ -277,7 +351,7 @@ pub const DeclGen = struct {
}
fn arithmeticTypeInfo(self: *DeclGen, ty: Type) !ArithmeticTypeInfo {
const target = self.module.getTarget();
const target = self.getTarget();
return switch (ty.zigTypeTag()) {
.Bool => ArithmeticTypeInfo{
.bits = 1, // Doesn't matter for this class.
@ -313,7 +387,7 @@ pub const DeclGen = struct {
/// Generate a constant representing `val`.
/// TODO: Deduplication?
fn genConstant(self: *DeclGen, src: LazySrcLoc, ty: Type, val: Value) Error!ResultId {
const target = self.module.getTarget();
const target = self.getTarget();
const code = &self.spv.binary.types_globals_constants;
const result_id = self.spv.allocResultId();
const result_type_id = try self.genType(src, ty);
@ -398,7 +472,7 @@ pub const DeclGen = struct {
return already_generated;
}
const target = self.module.getTarget();
const target = self.getTarget();
const code = &self.spv.binary.types_globals_constants;
const result_id = self.spv.allocResultId();
@ -587,7 +661,7 @@ pub const DeclGen = struct {
.breakpoint => null,
.condbr => try self.genCondBr(inst.castTag(.condbr).?),
.constant => unreachable,
.dbg_stmt => null,
.dbg_stmt => try self.genDbgStmt(inst.castTag(.dbg_stmt).?),
.load => try self.genLoad(inst.castTag(.load).?),
.loop => try self.genLoop(inst.castTag(.loop).?),
.ret => try self.genRet(inst.castTag(.ret).?),
@ -748,7 +822,7 @@ pub const DeclGen = struct {
const label_id = self.spv.allocResultId();
// 4 chosen as arbitrary initial capacity.
var incoming_blocks = try std.ArrayListUnmanaged(IncomingBlock).initCapacity(self.module.gpa, 4);
var incoming_blocks = try std.ArrayListUnmanaged(IncomingBlock).initCapacity(self.spv.gpa, 4);
try self.blocks.putNoClobber(inst, .{
.label_id = label_id,
@ -756,7 +830,7 @@ pub const DeclGen = struct {
});
defer {
self.blocks.removeAssertDiscard(inst);
incoming_blocks.deinit(self.module.gpa);
incoming_blocks.deinit(self.spv.gpa);
}
try self.genBody(inst.body);
@ -792,7 +866,7 @@ pub const DeclGen = struct {
if (inst.operand.ty.hasCodeGenBits()) {
const operand_id = try self.resolve(inst.operand);
// current_block_label_id should not be undefined here, lest there is a br or br_void in the function's body.
try target.incoming_blocks.append(self.module.gpa, .{
try target.incoming_blocks.append(self.spv.gpa, .{
.src_label_id = self.current_block_label_id,
.break_value_id = operand_id
});
@ -836,6 +910,12 @@ pub const DeclGen = struct {
return null;
}
fn genDbgStmt(self: *DeclGen, inst: *Inst.DbgStmt) !?ResultId {
const src_fname_id = try self.spv.resolveSourceFileName(self.decl);
try writeInstruction(&self.spv.binary.fn_decls, .OpLine, &[_]Word{ src_fname_id, inst.line, inst.column });
return null;
}
fn genLoad(self: *DeclGen, inst: *Inst.UnOp) !ResultId {
const operand_id = try self.resolve(inst.operand);

View File

@ -132,7 +132,7 @@ pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
const module = self.base.options.module.?;
const target = comp.getTarget();
var spv = codegen.SPIRVModule.init(self.base.allocator);
var spv = codegen.SPIRVModule.init(self.base.allocator, module);
defer spv.deinit();
// Allocate an ID for every declaration before generating code,
@ -152,7 +152,7 @@ pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
// Now, actually generate the code for all declarations.
{
var decl_gen = codegen.DeclGen.init(self.base.allocator, module, &spv);
var decl_gen = codegen.DeclGen.init(self.base.allocator, &spv);
defer decl_gen.deinit();
for (self.decl_table.items()) |entry| {
@ -166,39 +166,45 @@ pub fn flushModule(self: *SpirV, comp: *Compilation) !void {
}
}
var binary = std.ArrayList(Word).init(self.base.allocator);
defer binary.deinit();
try writeCapabilities(&spv.binary.capabilities_and_extensions, target);
try writeMemoryModel(&spv.binary.capabilities_and_extensions, target);
try binary.appendSlice(&[_]Word{
const header = [_]Word{
spec.magic_number,
(spec.version.major << 16) | (spec.version.minor << 8),
0, // TODO: Register Zig compiler magic number.
spv.resultIdBound(), // ID bound.
spv.resultIdBound(),
0, // Schema (currently reserved for future use in the SPIR-V spec).
});
try writeCapabilities(&binary, target);
try writeMemoryModel(&binary, target);
};
// Note: The order of adding sections to the final binary
// follows the SPIR-V logical module format!
var all_buffers = [_]std.os.iovec_const{
wordsToIovConst(binary.items),
wordsToIovConst(spv.binary.types_globals_constants.items),
wordsToIovConst(spv.binary.fn_decls.items),
const buffers = &[_][]const Word{
&header,
spv.binary.capabilities_and_extensions.items,
spv.binary.debug_strings.items,
spv.binary.types_globals_constants.items,
spv.binary.fn_decls.items,
};
const file = self.base.file.?;
const bytes = std.mem.sliceAsBytes(binary.items);
var iovc_buffers: [buffers.len]std.os.iovec_const = undefined;
for (iovc_buffers) |*iovc, i| {
const bytes = std.mem.sliceAsBytes(buffers[i]);
iovc.* = .{
.iov_base = bytes.ptr,
.iov_len = bytes.len
};
}
var file_size: u64 = 0;
for (all_buffers) |iov| {
for (iovc_buffers) |iov| {
file_size += iov.iov_len;
}
const file = self.base.file.?;
try file.seekTo(0);
try file.setEndPos(file_size);
try file.pwritevAll(&all_buffers, 0);
try file.pwritevAll(&iovc_buffers, 0);
}
fn writeCapabilities(binary: *std.ArrayList(Word), target: std.Target) !void {
@ -235,11 +241,3 @@ fn writeMemoryModel(binary: *std.ArrayList(Word), target: std.Target) !void {
@enumToInt(addressing_model), @enumToInt(memory_model),
});
}
fn wordsToIovConst(words: []const Word) std.os.iovec_const {
const bytes = std.mem.sliceAsBytes(words);
return .{
.iov_base = bytes.ptr,
.iov_len = bytes.len,
};
}