From c4b295bb6e9fcb1d02122cb70c95019c81ae543e Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 15 Apr 2023 16:17:25 +0200 Subject: [PATCH 1/4] wasm: implement `cmp_lt_errors_len` instruction Creates a global undefined symbol when this instruction is called. The linker will then resolve it as a lazy symbol, ensuring it is only generated when the symbol was created. In `flush` it will then generate the function as only then, all errors are known and we can generate the function body. This logic allows us to re-use the same functionality of linker-synthetic-functions. --- src/arch/wasm/CodeGen.zig | 8 +++++-- src/link/Wasm.zig | 45 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index b94d3993f9..0d73e35185 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/src/arch/wasm/CodeGen.zig @@ -3338,9 +3338,13 @@ fn airCmpVector(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { fn airCmpLtErrorsLen(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { const un_op = func.air.instructions.items(.data)[inst].un_op; const operand = try func.resolveInst(un_op); + const sym_index = try func.bin_file.getGlobalSymbol("__zig_lt_errors_len", null); - _ = operand; - return func.fail("TODO implement airCmpLtErrorsLen for wasm", .{}); + try func.emitWValue(operand); + try func.addLabel(.call, sym_index); + const result = try func.allocLocal(Type.bool); + try func.addLabel(.local_set, result.local.value); + return func.finishAir(inst, result, &.{un_op}); } fn airBr(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 2125a8faaa..1b7c43a6a0 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -1209,6 +1209,10 @@ fn resolveLazySymbols(wasm: *Wasm) !void { try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); } } + if (wasm.undefs.fetchSwapRemove("__zig_lt_errors_len")) |kv| { + const loc = try wasm.createSyntheticSymbol("__zig_lt_errors_len", .function); + try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); + } } // Tries to find a global symbol by its name. Returns null when not found, @@ -2185,6 +2189,46 @@ fn setupInitFunctions(wasm: *Wasm) !void { std.sort.sort(InitFuncLoc, wasm.init_funcs.items, {}, InitFuncLoc.lessThan); } +/// Generates the function which verifies if an integer value is less than the +/// amount of error values. This will only be generated if the symbol exists. +fn setupLtErrorsLenFunction(wasm: *Wasm) !void { + if (wasm.findGlobalSymbol("__zig_lt_errors_len") == null) return; + const errors_len = wasm.base.options.module.?.global_error_set.count(); + + var body_list = std.ArrayList(u8).init(wasm.base.allocator); + defer body_list.deinit(); + const writer = body_list.writer(); + + { + // generates bytecode for the following function: + // fn (index: u16) bool { + // return index < errors_len; + // } + + // no locals + try leb.writeULEB128(writer, @as(u32, 0)); + + // get argument + try writer.writeByte(std.wasm.opcode(.local_get)); + try leb.writeULEB128(writer, @as(u32, 0)); + + // get error length + try writer.writeByte(std.wasm.opcode(.i32_const)); + try leb.writeULEB128(writer, @intCast(u32, errors_len)); + + try writer.writeByte(std.wasm.opcode(.i32_lt_u)); + + // stack values are implicit return values so keep the value + // on the stack and end the function. + + // end function + try writer.writeByte(std.wasm.opcode(.end)); + } + + const func_type: std.wasm.Type = .{ .params = &.{std.wasm.Valtype.i32}, .returns = &.{std.wasm.Valtype.i32} }; + try wasm.createSyntheticFunction("__zig_lt_errors_len", func_type, &body_list); +} + /// Creates a function body for the `__wasm_call_ctors` symbol. /// Loops over all constructors found in `init_funcs` and calls them /// respectively based on their priority which was sorted by `setupInitFunctions`. @@ -3379,6 +3423,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod try wasm.setupInitMemoryFunction(); try wasm.setupTLSRelocationsFunction(); try wasm.initializeTLSFunction(); + try wasm.setupLtErrorsLenFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); } From 27a41413f71b9b2d2fb15ea14a96983b4ba9bd2e Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sat, 15 Apr 2023 16:23:41 +0200 Subject: [PATCH 2/4] wasm: enable `@intToError` test --- test/behavior/cast.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/test/behavior/cast.zig b/test/behavior/cast.zig index bdff7c4de4..fc364da0b8 100644 --- a/test/behavior/cast.zig +++ b/test/behavior/cast.zig @@ -401,7 +401,6 @@ test "expected [*c]const u8, found [*:0]const u8" { } test "explicit cast from integer to error type" { - if (builtin.zig_backend == .stage2_wasm) return error.SkipZigTest; if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; if (builtin.zig_backend == .stage2_arm) return error.SkipZigTest; // TODO if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO From d4ceb12ae9d409dbd52c1f5c96312a1e6ad7d6bc Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Sun, 16 Apr 2023 15:10:45 +0200 Subject: [PATCH 3/4] wasm: implement `error_set_has_value` This implements the safety check for error casts. The instruction generates a jump table with 2 possibilities. The operand is used as an index into the jump table. For cases where the value does not exist within the error set, it will generate a jump to the 'false' block. For cases where it does exist, it will generate a jump to the 'true' block. By calculating the highest and lowest value we can keep the jump table smaller, as it doesn't need to contain an index into the entire error set. --- src/Module.zig | 2 +- src/arch/wasm/CodeGen.zig | 85 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/Module.zig b/src/Module.zig index fa91e8c1ed..27f7b24e7a 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -6626,7 +6626,7 @@ pub fn backendSupportsFeature(mod: Module, feature: Feature) bool { .safety_check_formatted => mod.comp.bin_file.options.use_llvm, .error_return_trace => mod.comp.bin_file.options.use_llvm, .is_named_enum_value => mod.comp.bin_file.options.use_llvm, - .error_set_has_value => mod.comp.bin_file.options.use_llvm, + .error_set_has_value => mod.comp.bin_file.options.use_llvm or mod.comp.bin_file.options.target.isWasm(), .field_reordering => mod.comp.bin_file.options.use_llvm, }; } diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index 0d73e35185..c0bab03428 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/src/arch/wasm/CodeGen.zig @@ -1946,6 +1946,8 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { .ret_addr => func.airRetAddr(inst), .tag_name => func.airTagName(inst), + .error_set_has_value => func.airErrorSetHasValue(inst), + .mul_sat, .mod, .assembly, @@ -1967,7 +1969,6 @@ fn genInst(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { .set_err_return_trace, .save_err_return_trace_index, .is_named_enum_value, - .error_set_has_value, .addrspace_cast, .vector_store_elem, .c_va_arg, @@ -6514,3 +6515,85 @@ fn getTagNameFunction(func: *CodeGen, enum_ty: Type) InnerError!u32 { const func_type = try genFunctype(arena, .Unspecified, &.{int_tag_ty}, slice_ty, func.target); return func.bin_file.createFunction(func_name, func_type, &body_list, &relocs); } + +fn airErrorSetHasValue(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { + const ty_op = func.air.instructions.items(.data)[inst].ty_op; + if (func.liveness.isUnused(inst)) return func.finishAir(inst, .none, &.{ty_op.operand}); + + const operand = try func.resolveInst(ty_op.operand); + const error_set_ty = func.air.getRefType(ty_op.ty); + const result = try func.allocLocal(Type.bool); + + const names = error_set_ty.errorSetNames(); + var values = try std.ArrayList(u32).initCapacity(func.gpa, names.len); + defer values.deinit(); + + const module = func.bin_file.base.options.module.?; + var lowest: ?u32 = null; + var highest: ?u32 = null; + for (names) |name| { + const err_int = module.global_error_set.get(name).?; + if (lowest) |*l| { + if (err_int < l.*) { + l.* = err_int; + } + } else { + lowest = err_int; + } + if (highest) |*h| { + if (err_int > h.*) { + highest = err_int; + } + } else { + highest = err_int; + } + + values.appendAssumeCapacity(err_int); + } + + // start block for 'true' branch + try func.startBlock(.block, wasm.block_empty); + // start block for 'false' branch + try func.startBlock(.block, wasm.block_empty); + // block for the jump table itself + try func.startBlock(.block, wasm.block_empty); + + // lower operand to determine jump table target + try func.emitWValue(operand); + try func.addImm32(@intCast(i32, lowest.?)); + try func.addTag(.i32_sub); + + // Account for default branch so always add '1' + const depth = @intCast(u32, highest.? - lowest.? + 1); + const jump_table: Mir.JumpTable = .{ .length = depth }; + const table_extra_index = try func.addExtra(jump_table); + try func.addInst(.{ .tag = .br_table, .data = .{ .payload = table_extra_index } }); + try func.mir_extra.ensureUnusedCapacity(func.gpa, depth); + + var value: u32 = lowest.?; + while (value <= highest.?) : (value += 1) { + const idx: u32 = blk: { + for (values.items) |val| { + if (val == value) break :blk 1; + } + break :blk 0; + }; + func.mir_extra.appendAssumeCapacity(idx); + } + try func.endBlock(); + + // 'false' branch (i.e. error set does not have value + // ensure we set local to 0 in case the local was re-used. + try func.addImm32(0); + try func.addLabel(.local_set, result.local.value); + try func.addLabel(.br, 1); + try func.endBlock(); + + // 'true' branch + try func.addImm32(1); + try func.addLabel(.local_set, result.local.value); + try func.addLabel(.br, 0); + try func.endBlock(); + + return func.finishAir(inst, result, &.{ty_op.operand}); +} From 6c1ab376ddcdbb05610487e5b813d42ff37da40d Mon Sep 17 00:00:00 2001 From: Luuk de Gram Date: Mon, 17 Apr 2023 20:05:24 +0200 Subject: [PATCH 4/4] wasm: store `__zig_lt_errors_len` in linear data Rather than using a function call to verify if an error fits within the global error set's length, we now store the error set' size in the .rodata segment of the linear memory and load that value onto the stack to check with the integer value. --- src/arch/wasm/CodeGen.zig | 12 +++---- src/link/Wasm.zig | 76 +++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig index c0bab03428..9bf39b73f1 100644 --- a/src/arch/wasm/CodeGen.zig +++ b/src/arch/wasm/CodeGen.zig @@ -3339,13 +3339,14 @@ fn airCmpVector(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { fn airCmpLtErrorsLen(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { const un_op = func.air.instructions.items(.data)[inst].un_op; const operand = try func.resolveInst(un_op); - const sym_index = try func.bin_file.getGlobalSymbol("__zig_lt_errors_len", null); + const sym_index = try func.bin_file.getGlobalSymbol("__zig_errors_len", null); + const errors_len = WValue{ .memory = sym_index }; try func.emitWValue(operand); - try func.addLabel(.call, sym_index); - const result = try func.allocLocal(Type.bool); - try func.addLabel(.local_set, result.local.value); - return func.finishAir(inst, result, &.{un_op}); + const errors_len_val = try func.load(errors_len, Type.err_int, 0); + const result = try func.cmp(.stack, errors_len_val, Type.err_int, .lt); + + return func.finishAir(inst, try result.toLocal(func, Type.bool), &.{un_op}); } fn airBr(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { @@ -6518,7 +6519,6 @@ fn getTagNameFunction(func: *CodeGen, enum_ty: Type) InnerError!u32 { fn airErrorSetHasValue(func: *CodeGen, inst: Air.Inst.Index) InnerError!void { const ty_op = func.air.instructions.items(.data)[inst].ty_op; - if (func.liveness.isUnused(inst)) return func.finishAir(inst, .none, &.{ty_op.operand}); const operand = try func.resolveInst(ty_op.operand); const error_set_ty = func.air.getRefType(ty_op.ty); diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index 1b7c43a6a0..0fe9ec5e3b 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -1209,9 +1209,10 @@ fn resolveLazySymbols(wasm: *Wasm) !void { try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); } } - if (wasm.undefs.fetchSwapRemove("__zig_lt_errors_len")) |kv| { - const loc = try wasm.createSyntheticSymbol("__zig_lt_errors_len", .function); + if (wasm.undefs.fetchSwapRemove("__zig_errors_len")) |kv| { + const loc = try wasm.createSyntheticSymbol("__zig_errors_len", .data); try wasm.discarded.putNoClobber(wasm.base.allocator, kv.value, loc); + _ = wasm.resolved_symbols.swapRemove(kv.value); } } @@ -2189,44 +2190,41 @@ fn setupInitFunctions(wasm: *Wasm) !void { std.sort.sort(InitFuncLoc, wasm.init_funcs.items, {}, InitFuncLoc.lessThan); } -/// Generates the function which verifies if an integer value is less than the -/// amount of error values. This will only be generated if the symbol exists. -fn setupLtErrorsLenFunction(wasm: *Wasm) !void { - if (wasm.findGlobalSymbol("__zig_lt_errors_len") == null) return; +/// Generates an atom containing the global error set' size. +/// This will only be generated if the symbol exists. +fn setupErrorsLen(wasm: *Wasm) !void { + const loc = wasm.findGlobalSymbol("__zig_errors_len") orelse return; + const errors_len = wasm.base.options.module.?.global_error_set.count(); + // overwrite existing atom if it already exists (maybe the error set has increased) + // if not, allcoate a new atom. + const atom_index = if (wasm.symbol_atom.get(loc)) |index| blk: { + const atom = wasm.getAtomPtr(index); + if (atom.next) |next_atom_index| { + const next_atom = wasm.getAtomPtr(next_atom_index); + next_atom.prev = atom.prev; + atom.next = null; + } + if (atom.prev) |prev_index| { + const prev_atom = wasm.getAtomPtr(prev_index); + prev_atom.next = atom.next; + atom.prev = null; + } + atom.deinit(wasm); + break :blk index; + } else new_atom: { + const atom_index = @intCast(Atom.Index, wasm.managed_atoms.items.len); + try wasm.symbol_atom.put(wasm.base.allocator, loc, atom_index); + try wasm.managed_atoms.append(wasm.base.allocator, undefined); + break :new_atom atom_index; + }; + const atom = wasm.getAtomPtr(atom_index); + atom.* = Atom.empty; + atom.sym_index = loc.index; + atom.size = 2; + try atom.code.writer(wasm.base.allocator).writeIntLittle(u16, @intCast(u16, errors_len)); - var body_list = std.ArrayList(u8).init(wasm.base.allocator); - defer body_list.deinit(); - const writer = body_list.writer(); - - { - // generates bytecode for the following function: - // fn (index: u16) bool { - // return index < errors_len; - // } - - // no locals - try leb.writeULEB128(writer, @as(u32, 0)); - - // get argument - try writer.writeByte(std.wasm.opcode(.local_get)); - try leb.writeULEB128(writer, @as(u32, 0)); - - // get error length - try writer.writeByte(std.wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, @intCast(u32, errors_len)); - - try writer.writeByte(std.wasm.opcode(.i32_lt_u)); - - // stack values are implicit return values so keep the value - // on the stack and end the function. - - // end function - try writer.writeByte(std.wasm.opcode(.end)); - } - - const func_type: std.wasm.Type = .{ .params = &.{std.wasm.Valtype.i32}, .returns = &.{std.wasm.Valtype.i32} }; - try wasm.createSyntheticFunction("__zig_lt_errors_len", func_type, &body_list); + try wasm.parseAtom(atom_index, .{ .data = .read_only }); } /// Creates a function body for the `__wasm_call_ctors` symbol. @@ -3361,6 +3359,7 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod // So we can rebuild the binary file on each incremental update defer wasm.resetState(); try wasm.setupInitFunctions(); + try wasm.setupErrorsLen(); try wasm.setupStart(); try wasm.setupImports(); if (wasm.base.options.module) |mod| { @@ -3423,7 +3422,6 @@ pub fn flushModule(wasm: *Wasm, comp: *Compilation, prog_node: *std.Progress.Nod try wasm.setupInitMemoryFunction(); try wasm.setupTLSRelocationsFunction(); try wasm.initializeTLSFunction(); - try wasm.setupLtErrorsLenFunction(); try wasm.setupExports(); try wasm.writeToFile(enabled_features, emit_features_count, arena); }