zig/src/arch/wasm/Emit.zig
Luuk de Gram f760272200 wasm: Debug info for lines + pro/epilogue
Maps lines and columns between wasm bytecode and Zig source code.
While this supports prologue and epilogue information, we need to add
support for performing relocations as the offsets are relative to the code section,
which means we must relocate it according to the atom offset's offset while keeping function count
in mind as well (due to leb128 encoding).
2022-05-09 18:51:46 +02:00

477 lines
20 KiB
Zig

//! Contains all logic to lower wasm MIR into its binary
//! or textual representation.
const Emit = @This();
const std = @import("std");
const Mir = @import("Mir.zig");
const link = @import("../../link.zig");
const Module = @import("../../Module.zig");
const codegen = @import("../../codegen.zig");
const leb128 = std.leb;
/// Contains our list of instructions
mir: Mir,
/// Reference to the file handler
bin_file: *link.File,
/// Possible error message. When set, the value is allocated and
/// must be freed manually.
error_msg: ?*Module.ErrorMsg = null,
/// The binary representation that will be emit by this module.
code: *std.ArrayList(u8),
/// List of allocated locals.
locals: []const u8,
/// The declaration that code is being generated for.
decl: *Module.Decl,
// Debug information
/// Holds the debug information for this emission
dbg_output: codegen.DebugInfoOutput,
/// Previous debug info line
prev_di_line: u32,
/// Previous debug info column
prev_di_column: u32,
/// Previous offset relative to code section
prev_di_offset: u32,
const InnerError = error{
OutOfMemory,
EmitFail,
};
pub fn emitMir(emit: *Emit) InnerError!void {
const mir_tags = emit.mir.instructions.items(.tag);
// write the locals in the prologue of the function body
// before we emit the function body when lowering MIR
try emit.emitLocals();
for (mir_tags) |tag, index| {
const inst = @intCast(u32, index);
switch (tag) {
// block instructions
.block => try emit.emitBlock(tag, inst),
.loop => try emit.emitBlock(tag, inst),
.dbg_line => try emit.emitDbgLine(inst),
.dbg_epilogue_begin => try emit.emitDbgEpilogueBegin(),
.dbg_prologue_end => try emit.emitDbgPrologueEnd(),
// branch instructions
.br_if => try emit.emitLabel(tag, inst),
.br_table => try emit.emitBrTable(inst),
.br => try emit.emitLabel(tag, inst),
// relocatables
.call => try emit.emitCall(inst),
.call_indirect => try emit.emitCallIndirect(inst),
.global_get => try emit.emitGlobal(tag, inst),
.global_set => try emit.emitGlobal(tag, inst),
.function_index => try emit.emitFunctionIndex(inst),
.memory_address => try emit.emitMemAddress(inst),
// immediates
.f32_const => try emit.emitFloat32(inst),
.f64_const => try emit.emitFloat64(inst),
.i32_const => try emit.emitImm32(inst),
.i64_const => try emit.emitImm64(inst),
// memory instructions
.i32_load => try emit.emitMemArg(tag, inst),
.i64_load => try emit.emitMemArg(tag, inst),
.f32_load => try emit.emitMemArg(tag, inst),
.f64_load => try emit.emitMemArg(tag, inst),
.i32_load8_s => try emit.emitMemArg(tag, inst),
.i32_load8_u => try emit.emitMemArg(tag, inst),
.i32_load16_s => try emit.emitMemArg(tag, inst),
.i32_load16_u => try emit.emitMemArg(tag, inst),
.i64_load8_s => try emit.emitMemArg(tag, inst),
.i64_load8_u => try emit.emitMemArg(tag, inst),
.i64_load16_s => try emit.emitMemArg(tag, inst),
.i64_load16_u => try emit.emitMemArg(tag, inst),
.i64_load32_s => try emit.emitMemArg(tag, inst),
.i64_load32_u => try emit.emitMemArg(tag, inst),
.i32_store => try emit.emitMemArg(tag, inst),
.i64_store => try emit.emitMemArg(tag, inst),
.f32_store => try emit.emitMemArg(tag, inst),
.f64_store => try emit.emitMemArg(tag, inst),
.i32_store8 => try emit.emitMemArg(tag, inst),
.i32_store16 => try emit.emitMemArg(tag, inst),
.i64_store8 => try emit.emitMemArg(tag, inst),
.i64_store16 => try emit.emitMemArg(tag, inst),
.i64_store32 => try emit.emitMemArg(tag, inst),
// Instructions with an index that do not require relocations
.local_get => try emit.emitLabel(tag, inst),
.local_set => try emit.emitLabel(tag, inst),
.local_tee => try emit.emitLabel(tag, inst),
.memory_grow => try emit.emitLabel(tag, inst),
.memory_size => try emit.emitLabel(tag, inst),
// no-ops
.end => try emit.emitTag(tag),
.@"return" => try emit.emitTag(tag),
.@"unreachable" => try emit.emitTag(tag),
.select => try emit.emitTag(tag),
// arithmetic
.i32_eqz => try emit.emitTag(tag),
.i32_eq => try emit.emitTag(tag),
.i32_ne => try emit.emitTag(tag),
.i32_lt_s => try emit.emitTag(tag),
.i32_lt_u => try emit.emitTag(tag),
.i32_gt_s => try emit.emitTag(tag),
.i32_gt_u => try emit.emitTag(tag),
.i32_le_s => try emit.emitTag(tag),
.i32_le_u => try emit.emitTag(tag),
.i32_ge_s => try emit.emitTag(tag),
.i32_ge_u => try emit.emitTag(tag),
.i64_eqz => try emit.emitTag(tag),
.i64_eq => try emit.emitTag(tag),
.i64_ne => try emit.emitTag(tag),
.i64_lt_s => try emit.emitTag(tag),
.i64_lt_u => try emit.emitTag(tag),
.i64_gt_s => try emit.emitTag(tag),
.i64_gt_u => try emit.emitTag(tag),
.i64_le_s => try emit.emitTag(tag),
.i64_le_u => try emit.emitTag(tag),
.i64_ge_s => try emit.emitTag(tag),
.i64_ge_u => try emit.emitTag(tag),
.f32_eq => try emit.emitTag(tag),
.f32_ne => try emit.emitTag(tag),
.f32_lt => try emit.emitTag(tag),
.f32_gt => try emit.emitTag(tag),
.f32_le => try emit.emitTag(tag),
.f32_ge => try emit.emitTag(tag),
.f64_eq => try emit.emitTag(tag),
.f64_ne => try emit.emitTag(tag),
.f64_lt => try emit.emitTag(tag),
.f64_gt => try emit.emitTag(tag),
.f64_le => try emit.emitTag(tag),
.f64_ge => try emit.emitTag(tag),
.i32_add => try emit.emitTag(tag),
.i32_sub => try emit.emitTag(tag),
.i32_mul => try emit.emitTag(tag),
.i32_div_s => try emit.emitTag(tag),
.i32_div_u => try emit.emitTag(tag),
.i32_and => try emit.emitTag(tag),
.i32_or => try emit.emitTag(tag),
.i32_xor => try emit.emitTag(tag),
.i32_shl => try emit.emitTag(tag),
.i32_shr_s => try emit.emitTag(tag),
.i32_shr_u => try emit.emitTag(tag),
.i64_add => try emit.emitTag(tag),
.i64_sub => try emit.emitTag(tag),
.i64_mul => try emit.emitTag(tag),
.i64_div_s => try emit.emitTag(tag),
.i64_div_u => try emit.emitTag(tag),
.i64_and => try emit.emitTag(tag),
.i64_or => try emit.emitTag(tag),
.i64_xor => try emit.emitTag(tag),
.i64_shl => try emit.emitTag(tag),
.i64_shr_s => try emit.emitTag(tag),
.i64_shr_u => try emit.emitTag(tag),
.f32_abs => try emit.emitTag(tag),
.f32_neg => try emit.emitTag(tag),
.f32_ceil => try emit.emitTag(tag),
.f32_floor => try emit.emitTag(tag),
.f32_trunc => try emit.emitTag(tag),
.f32_nearest => try emit.emitTag(tag),
.f32_sqrt => try emit.emitTag(tag),
.f32_add => try emit.emitTag(tag),
.f32_sub => try emit.emitTag(tag),
.f32_mul => try emit.emitTag(tag),
.f32_div => try emit.emitTag(tag),
.f32_min => try emit.emitTag(tag),
.f32_max => try emit.emitTag(tag),
.f32_copysign => try emit.emitTag(tag),
.f64_abs => try emit.emitTag(tag),
.f64_neg => try emit.emitTag(tag),
.f64_ceil => try emit.emitTag(tag),
.f64_floor => try emit.emitTag(tag),
.f64_trunc => try emit.emitTag(tag),
.f64_nearest => try emit.emitTag(tag),
.f64_sqrt => try emit.emitTag(tag),
.f64_add => try emit.emitTag(tag),
.f64_sub => try emit.emitTag(tag),
.f64_mul => try emit.emitTag(tag),
.f64_div => try emit.emitTag(tag),
.f64_min => try emit.emitTag(tag),
.f64_max => try emit.emitTag(tag),
.f64_copysign => try emit.emitTag(tag),
.i32_wrap_i64 => try emit.emitTag(tag),
.i64_extend_i32_s => try emit.emitTag(tag),
.i64_extend_i32_u => try emit.emitTag(tag),
.i32_extend8_s => try emit.emitTag(tag),
.i32_extend16_s => try emit.emitTag(tag),
.i64_extend8_s => try emit.emitTag(tag),
.i64_extend16_s => try emit.emitTag(tag),
.i64_extend32_s => try emit.emitTag(tag),
.f32_demote_f64 => try emit.emitTag(tag),
.f64_promote_f32 => try emit.emitTag(tag),
.i32_reinterpret_f32 => try emit.emitTag(tag),
.i64_reinterpret_f64 => try emit.emitTag(tag),
.f32_reinterpret_i32 => try emit.emitTag(tag),
.f64_reinterpret_i64 => try emit.emitTag(tag),
.i32_trunc_f32_s => try emit.emitTag(tag),
.i32_trunc_f32_u => try emit.emitTag(tag),
.i32_trunc_f64_s => try emit.emitTag(tag),
.i32_trunc_f64_u => try emit.emitTag(tag),
.i64_trunc_f32_s => try emit.emitTag(tag),
.i64_trunc_f32_u => try emit.emitTag(tag),
.i64_trunc_f64_s => try emit.emitTag(tag),
.i64_trunc_f64_u => try emit.emitTag(tag),
.f32_convert_i32_s => try emit.emitTag(tag),
.f32_convert_i32_u => try emit.emitTag(tag),
.f32_convert_i64_s => try emit.emitTag(tag),
.f32_convert_i64_u => try emit.emitTag(tag),
.f64_convert_i32_s => try emit.emitTag(tag),
.f64_convert_i32_u => try emit.emitTag(tag),
.f64_convert_i64_s => try emit.emitTag(tag),
.f64_convert_i64_u => try emit.emitTag(tag),
.i32_rem_s => try emit.emitTag(tag),
.i32_rem_u => try emit.emitTag(tag),
.i64_rem_s => try emit.emitTag(tag),
.i64_rem_u => try emit.emitTag(tag),
.i32_popcnt => try emit.emitTag(tag),
.i64_popcnt => try emit.emitTag(tag),
.i32_clz => try emit.emitTag(tag),
.i32_ctz => try emit.emitTag(tag),
.i64_clz => try emit.emitTag(tag),
.i64_ctz => try emit.emitTag(tag),
.extended => try emit.emitExtended(inst),
}
}
}
fn offset(self: Emit) u32 {
return @intCast(u32, self.code.items.len);
}
fn fail(emit: *Emit, comptime format: []const u8, args: anytype) InnerError {
@setCold(true);
std.debug.assert(emit.error_msg == null);
// TODO: Determine the source location.
emit.error_msg = try Module.ErrorMsg.create(emit.bin_file.allocator, emit.decl.srcLoc(), format, args);
return error.EmitFail;
}
fn emitLocals(emit: *Emit) !void {
const writer = emit.code.writer();
try leb128.writeULEB128(writer, @intCast(u32, emit.locals.len));
// emit the actual locals amount
for (emit.locals) |local| {
try leb128.writeULEB128(writer, @as(u32, 1));
try writer.writeByte(local);
}
}
fn emitTag(emit: *Emit, tag: Mir.Inst.Tag) !void {
try emit.code.append(@enumToInt(tag));
}
fn emitBlock(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) !void {
const block_type = emit.mir.instructions.items(.data)[inst].block_type;
try emit.code.append(@enumToInt(tag));
try emit.code.append(block_type);
}
fn emitBrTable(emit: *Emit, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const extra = emit.mir.extraData(Mir.JumpTable, extra_index);
const labels = emit.mir.extra[extra.end..][0..extra.data.length];
const writer = emit.code.writer();
try emit.code.append(std.wasm.opcode(.br_table));
try leb128.writeULEB128(writer, extra.data.length - 1); // Default label is not part of length/depth
for (labels) |label| {
try leb128.writeULEB128(writer, label);
}
}
fn emitLabel(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) !void {
const label = emit.mir.instructions.items(.data)[inst].label;
try emit.code.append(@enumToInt(tag));
try leb128.writeULEB128(emit.code.writer(), label);
}
fn emitGlobal(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) !void {
const label = emit.mir.instructions.items(.data)[inst].label;
try emit.code.append(@enumToInt(tag));
var buf: [5]u8 = undefined;
leb128.writeUnsignedFixed(5, &buf, label);
const global_offset = emit.offset();
try emit.code.appendSlice(&buf);
// globals can have index 0 as it represents the stack pointer
try emit.decl.link.wasm.relocs.append(emit.bin_file.allocator, .{
.index = label,
.offset = global_offset,
.relocation_type = .R_WASM_GLOBAL_INDEX_LEB,
});
}
fn emitImm32(emit: *Emit, inst: Mir.Inst.Index) !void {
const value: i32 = emit.mir.instructions.items(.data)[inst].imm32;
try emit.code.append(std.wasm.opcode(.i32_const));
try leb128.writeILEB128(emit.code.writer(), value);
}
fn emitImm64(emit: *Emit, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const value = emit.mir.extraData(Mir.Imm64, extra_index);
try emit.code.append(std.wasm.opcode(.i64_const));
try leb128.writeILEB128(emit.code.writer(), @bitCast(i64, value.data.toU64()));
}
fn emitFloat32(emit: *Emit, inst: Mir.Inst.Index) !void {
const value: f32 = emit.mir.instructions.items(.data)[inst].float32;
try emit.code.append(std.wasm.opcode(.f32_const));
try emit.code.writer().writeIntLittle(u32, @bitCast(u32, value));
}
fn emitFloat64(emit: *Emit, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const value = emit.mir.extraData(Mir.Float64, extra_index);
try emit.code.append(std.wasm.opcode(.f64_const));
try emit.code.writer().writeIntLittle(u64, value.data.toU64());
}
fn emitMemArg(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const mem_arg = emit.mir.extraData(Mir.MemArg, extra_index).data;
try emit.code.append(@enumToInt(tag));
// wasm encodes alignment as power of 2, rather than natural alignment
const encoded_alignment = @ctz(u32, mem_arg.alignment);
try leb128.writeULEB128(emit.code.writer(), encoded_alignment);
try leb128.writeULEB128(emit.code.writer(), mem_arg.offset);
}
fn emitCall(emit: *Emit, inst: Mir.Inst.Index) !void {
const label = emit.mir.instructions.items(.data)[inst].label;
try emit.code.append(std.wasm.opcode(.call));
const call_offset = emit.offset();
var buf: [5]u8 = undefined;
leb128.writeUnsignedFixed(5, &buf, label);
try emit.code.appendSlice(&buf);
if (label != 0) {
try emit.decl.link.wasm.relocs.append(emit.bin_file.allocator, .{
.offset = call_offset,
.index = label,
.relocation_type = .R_WASM_FUNCTION_INDEX_LEB,
});
}
}
fn emitCallIndirect(emit: *Emit, inst: Mir.Inst.Index) !void {
const type_index = emit.mir.instructions.items(.data)[inst].label;
try emit.code.append(std.wasm.opcode(.call_indirect));
// NOTE: If we remove unused function types in the future for incremental
// linking, we must also emit a relocation for this `type_index`
try leb128.writeULEB128(emit.code.writer(), type_index);
try leb128.writeULEB128(emit.code.writer(), @as(u32, 0)); // TODO: Emit relocation for table index
}
fn emitFunctionIndex(emit: *Emit, inst: Mir.Inst.Index) !void {
const symbol_index = emit.mir.instructions.items(.data)[inst].label;
try emit.code.append(std.wasm.opcode(.i32_const));
const index_offset = emit.offset();
var buf: [5]u8 = undefined;
leb128.writeUnsignedFixed(5, &buf, symbol_index);
try emit.code.appendSlice(&buf);
if (symbol_index != 0) {
try emit.decl.link.wasm.relocs.append(emit.bin_file.allocator, .{
.offset = index_offset,
.index = symbol_index,
.relocation_type = .R_WASM_TABLE_INDEX_SLEB,
});
}
}
fn emitMemAddress(emit: *Emit, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const mem = emit.mir.extraData(Mir.Memory, extra_index).data;
const mem_offset = emit.offset() + 1;
const is_wasm32 = emit.bin_file.options.target.cpu.arch == .wasm32;
if (is_wasm32) {
try emit.code.append(std.wasm.opcode(.i32_const));
var buf: [5]u8 = undefined;
leb128.writeUnsignedFixed(5, &buf, mem.pointer);
try emit.code.appendSlice(&buf);
} else {
try emit.code.append(std.wasm.opcode(.i64_const));
var buf: [10]u8 = undefined;
leb128.writeUnsignedFixed(10, &buf, mem.pointer);
try emit.code.appendSlice(&buf);
}
if (mem.pointer != 0) {
try emit.decl.link.wasm.relocs.append(emit.bin_file.allocator, .{
.offset = mem_offset,
.index = mem.pointer,
.relocation_type = if (is_wasm32) .R_WASM_MEMORY_ADDR_LEB else .R_WASM_MEMORY_ADDR_LEB64,
.addend = mem.offset,
});
}
}
fn emitExtended(emit: *Emit, inst: Mir.Inst.Index) !void {
const opcode = emit.mir.instructions.items(.secondary)[inst];
switch (@intToEnum(std.wasm.PrefixedOpcode, opcode)) {
.memory_fill => try emit.emitMemFill(),
else => |tag| return emit.fail("TODO: Implement extension instruction: {s}\n", .{@tagName(tag)}),
}
}
fn emitMemFill(emit: *Emit) !void {
try emit.code.append(0xFC);
try emit.code.append(0x0B);
// When multi-memory proposal reaches phase 4, we
// can emit a different memory index here.
// For now we will always emit index 0.
try leb128.writeULEB128(emit.code.writer(), @as(u32, 0));
}
fn emitDbgLine(emit: *Emit, inst: Mir.Inst.Index) !void {
const extra_index = emit.mir.instructions.items(.data)[inst].payload;
const dbg_line = emit.mir.extraData(Mir.DbgLineColumn, extra_index).data;
try emit.dbgAdvancePCAndLine(dbg_line.line, dbg_line.column);
}
fn dbgAdvancePCAndLine(emit: *Emit, line: u32, column: u32) !void {
if (emit.dbg_output != .dwarf) return;
const dbg_line = &emit.dbg_output.dwarf.dbg_line;
try dbg_line.ensureUnusedCapacity(11);
dbg_line.appendAssumeCapacity(std.dwarf.LNS.advance_pc);
// TODO: This must emit a relocation to calculate the offset relative
// to the code section start.
leb128.writeULEB128(dbg_line.writer(), emit.offset() - emit.prev_di_offset) catch unreachable;
const delta_line = @intCast(i32, line) - @intCast(i32, emit.prev_di_line);
if (delta_line != 0) {
dbg_line.appendAssumeCapacity(std.dwarf.LNS.advance_line);
leb128.writeILEB128(dbg_line.writer(), delta_line) catch unreachable;
}
dbg_line.appendAssumeCapacity(std.dwarf.LNS.copy);
emit.prev_di_line = line;
emit.prev_di_column = column;
emit.prev_di_offset = emit.offset();
}
fn emitDbgPrologueEnd(emit: *Emit) !void {
if (emit.dbg_output != .dwarf) return;
try emit.dbg_output.dwarf.dbg_line.append(std.dwarf.LNS.set_prologue_end);
try emit.dbgAdvancePCAndLine(emit.prev_di_line, emit.prev_di_column);
}
fn emitDbgEpilogueBegin(emit: *Emit) !void {
if (emit.dbg_output != .dwarf) return;
try emit.dbg_output.dwarf.dbg_line.append(std.dwarf.LNS.set_epilogue_begin);
try emit.dbgAdvancePCAndLine(emit.prev_di_line, emit.prev_di_column);
}