wasm: Implement @errorName

This implements the `error_name` instruction, which is emit for runtime `@errorName` callsites.

The implementation works by creating 2 symbols and corresponding atoms.
The initial symbol contains a table which each element consisting of a slice where the ptr field
points towards the error name, and the len field contains the error name length without the sentinel.

The secondary symbol contains a list of all error names from the global error set.

During the error_name instruction, we first get a pointer to the first symbol.
Then based on the operand we perform pointer arithmetic, to get the correct index into this table.
e.g. error index 2 = ptr + (2 * ptr size). The result of this will be stored in a local
and then returned as instruction result.

During `flush()` we populate the error names table by looping over the global error set
and creating a relocation for each error name. This relocation is appended to the table symbol.
Then finally, this name is written to the names list itself.

Finally, both symbols' atom are allocated within the rest of the binary.
When no error name is referenced, the `error_name_symbol` is never set, and therefore
no error name table will be emit into the final binary.
This commit is contained in:
Luuk de Gram
2022-03-22 21:20:36 +01:00
parent b872539a13
commit 49051c0651
2 changed files with 171 additions and 1 deletions

View File

@@ -1403,6 +1403,7 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
.wrap_errunion_payload => self.airWrapErrUnionPayload(inst),
.wrap_errunion_err => self.airWrapErrUnionErr(inst),
.errunion_payload_ptr_set => self.airErrUnionPayloadPtrSet(inst),
.error_name => self.airErrorName(inst),
.wasm_memory_size => self.airWasmMemorySize(inst),
.wasm_memory_grow => self.airWasmMemoryGrow(inst),
@@ -1458,7 +1459,6 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
.atomic_store_seq_cst,
.atomic_rmw,
.tag_name,
.error_name,
.mul_add,
// For these 4, probably best to wait until https://github.com/ziglang/zig/issues/10248
@@ -3618,3 +3618,46 @@ fn airPopcount(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
try self.addLabel(.local_set, result.local);
return result;
}
fn airErrorName(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
if (self.liveness.isUnused(inst)) return WValue{ .none = {} };
const un_op = self.air.instructions.items(.data)[inst].un_op;
const operand = try self.resolveInst(un_op);
// First retrieve the symbol index to the error name table
// that will be used to emit a relocation for the pointer
// to the error name table.
//
// Each entry to this table is a slice (ptr+len).
// The operand in this instruction represents the index within this table.
// This means to get the final name, we emit the base pointer and then perform
// pointer arithmetic to find the pointer to this slice and return that.
//
// As the names are global and the slice elements are constant, we do not have
// to make a copy of the ptr+value but can point towards them directly.
const error_table_symbol = try self.bin_file.getErrorTableSymbol();
const name_ty = Type.initTag(.const_slice_u8_sentinel_0);
const abi_size = name_ty.abiSize(self.target);
const error_name_value: WValue = .{ .memory = error_table_symbol }; // emitting this will create a relocation
try self.emitWValue(error_name_value);
try self.emitWValue(operand);
switch (self.arch()) {
.wasm32 => {
try self.addImm32(@bitCast(i32, @intCast(u32, abi_size)));
try self.addTag(.i32_mul);
try self.addTag(.i32_add);
},
.wasm64 => {
try self.addImm64(abi_size);
try self.addTag(.i64_mul);
try self.addTag(.i64_add);
},
else => unreachable,
}
const result_ptr = try self.allocLocal(Type.usize);
try self.addLabel(.local_set, result_ptr.local);
return result_ptr;
}

View File

@@ -123,6 +123,13 @@ symbol_atom: std.AutoHashMapUnmanaged(SymbolLoc, *Atom) = .{},
/// Note: The value represents the offset into the string table, rather than the actual string.
export_names: std.AutoHashMapUnmanaged(SymbolLoc, u32) = .{},
/// Represents the symbol index of the error name table
/// When this is `null`, no code references an error using runtime `@errorName`.
/// During initializion, a symbol with corresponding atom will be created that is
/// used to perform relocations to the pointer of this table.
/// The actual table is populated during `flush`.
error_table_symbol: ?u32 = null,
pub const Segment = struct {
alignment: u32,
size: u32,
@@ -1322,6 +1329,123 @@ pub fn getMatchingSegment(self: *Wasm, object_index: u16, relocatable_index: u32
}
}
/// Returns the symbol index of the error name table.
///
/// When the symbol does not yet exist, it will create a new one instead.
pub fn getErrorTableSymbol(self: *Wasm) !u32 {
if (self.error_table_symbol) |symbol| {
return symbol;
}
// no error was referenced yet, so create a new symbol and atom for it
// and then return said symbol's index. The final table will be populated
// during `flush` when we know all possible error names.
// As sym_index '0' is reserved, we use it for our stack pointer symbol
const symbol_index = self.symbols_free_list.popOrNull() orelse blk: {
const index = @intCast(u32, self.symbols.items.len);
_ = try self.symbols.addOne(self.base.allocator);
break :blk index;
};
const sym_name = try self.string_table.put(self.base.allocator, "__zig_err_name_table");
const symbol = &self.symbols.items[symbol_index];
symbol.* = .{
.name = sym_name,
.tag = .data,
.flags = 0,
.index = 0,
};
symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
const slice_ty = Type.initTag(.const_slice_u8_sentinel_0);
const atom = try self.base.allocator.create(Atom);
atom.* = Atom.empty;
atom.sym_index = symbol_index;
atom.alignment = slice_ty.abiAlignment(self.base.options.target);
try self.managed_atoms.append(self.base.allocator, atom);
const loc = atom.symbolLoc();
try self.resolved_symbols.put(self.base.allocator, loc, {});
try self.symbol_atom.put(self.base.allocator, loc, atom);
log.debug("Error name table was created with symbol index: ({d})", .{symbol_index});
self.error_table_symbol = symbol_index;
return symbol_index;
}
/// Populates the error name table, when `error_table_symbol` is not null.
///
/// This creates a table that consists of pointers and length to each error name.
/// The table is what is being pointed to within the runtime bodies that are generated.
fn populateErrorNameTable(self: *Wasm) !void {
const symbol_index = self.error_table_symbol orelse return;
const atom: *Atom = self.symbol_atom.get(.{ .file = null, .index = symbol_index }).?;
// Rather than creating a symbol for each individual error name,
// we create a symbol for the entire region of error names. We then calculate
// the pointers into the list using addends which are appended to the relocation.
const names_atom = try self.base.allocator.create(Atom);
names_atom.* = Atom.empty;
try self.managed_atoms.append(self.base.allocator, names_atom);
const names_symbol_index = self.symbols_free_list.popOrNull() orelse blk: {
const index = @intCast(u32, self.symbols.items.len);
_ = try self.symbols.addOne(self.base.allocator);
break :blk index;
};
names_atom.sym_index = names_symbol_index;
names_atom.alignment = 1;
const sym_name = try self.string_table.put(self.base.allocator, "__zig_err_names");
const names_symbol = &self.symbols.items[names_symbol_index];
names_symbol.* = .{
.name = sym_name,
.tag = .data,
.flags = 0,
.index = 0,
};
names_symbol.setFlag(.WASM_SYM_VISIBILITY_HIDDEN);
log.debug("Populating error names", .{});
// Addend for each relocation to the table
var addend: u32 = 0;
const module = self.base.options.module.?;
for (module.error_name_list.items) |error_name| {
const len = @intCast(u32, error_name.len + 1); // names are 0-termianted
const slice_ty = Type.initTag(.const_slice_u8_sentinel_0);
const offset = @intCast(u32, atom.code.items.len);
// first we create the data for the slice of the name
try atom.code.appendNTimes(self.base.allocator, 0, 4); // ptr to name, will be relocated
try atom.code.writer(self.base.allocator).writeIntLittle(u32, len - 1);
// create relocation to the error name
try atom.relocs.append(self.base.allocator, .{
.index = names_symbol_index,
.relocation_type = .R_WASM_MEMORY_ADDR_I32,
.offset = offset,
.addend = addend,
});
atom.size += @intCast(u32, slice_ty.abiSize(self.base.options.target));
addend += len;
// as we updated the error name table, we now store the actual name within the names atom
try names_atom.code.ensureUnusedCapacity(self.base.allocator, len);
names_atom.code.appendSliceAssumeCapacity(error_name);
names_atom.code.appendAssumeCapacity(0);
log.debug("Populated error name: '{s}'", .{error_name});
}
names_atom.size = addend;
const name_loc = names_atom.symbolLoc();
try self.resolved_symbols.put(self.base.allocator, name_loc, {});
try self.symbol_atom.put(self.base.allocator, name_loc, names_atom);
// link the atoms with the rest of the binary so they can be allocated
// and relocations will be performed.
try self.parseAtom(atom, .data);
try self.parseAtom(names_atom, .data);
}
fn resetState(self: *Wasm) void {
for (self.segment_info.items) |*segment_info| {
self.base.allocator.free(segment_info.name);
@@ -1373,6 +1497,9 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
}
}
// ensure the error names table is populated when an error name is referenced
try self.populateErrorNameTable();
// The amount of sections that will be written
var section_count: u32 = 0;
// Index of the code section. Used to tell relocation table where the section lives.