diff --git a/CMakeLists.txt b/CMakeLists.txt index 1b427bb30b..3e2614a7c9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -555,6 +555,7 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/src/arch/arm/bits.zig" "${CMAKE_SOURCE_DIR}/src/arch/riscv64/bits.zig" "${CMAKE_SOURCE_DIR}/src/arch/x86_64/bits.zig" + "${CMAKE_SOURCE_DIR}/src/arch/wasm/CodeGen.zig" "${CMAKE_SOURCE_DIR}/src/clang.zig" "${CMAKE_SOURCE_DIR}/src/clang_options.zig" "${CMAKE_SOURCE_DIR}/src/clang_options_data.zig" @@ -562,7 +563,6 @@ set(ZIG_STAGE2_SOURCES "${CMAKE_SOURCE_DIR}/src/codegen/c.zig" "${CMAKE_SOURCE_DIR}/src/codegen/llvm.zig" "${CMAKE_SOURCE_DIR}/src/codegen/llvm/bindings.zig" - "${CMAKE_SOURCE_DIR}/src/codegen/wasm.zig" "${CMAKE_SOURCE_DIR}/src/glibc.zig" "${CMAKE_SOURCE_DIR}/src/introspect.zig" "${CMAKE_SOURCE_DIR}/src/libc_installation.zig" diff --git a/src/arch/wasm/CodeGen.zig b/src/arch/wasm/CodeGen.zig new file mode 100644 index 0000000000..adfec99d49 --- /dev/null +++ b/src/arch/wasm/CodeGen.zig @@ -0,0 +1,1710 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const assert = std.debug.assert; +const testing = std.testing; +const leb = std.leb; +const mem = std.mem; +const wasm = std.wasm; + +const Module = @import("../../Module.zig"); +const Decl = Module.Decl; +const Type = @import("../../type.zig").Type; +const Value = @import("../../value.zig").Value; +const Compilation = @import("../../Compilation.zig"); +const LazySrcLoc = Module.LazySrcLoc; +const link = @import("../../link.zig"); +const TypedValue = @import("../../TypedValue.zig"); +const Air = @import("../../Air.zig"); +const Liveness = @import("../../Liveness.zig"); +const Mir = @import("Mir.zig"); +const Emit = @import("Emit.zig"); + +/// Wasm Value, created when generating an instruction +const WValue = union(enum) { + /// May be referenced but is unused + none: void, + /// Index of the local variable + local: u32, + /// Holds a memoized typed value + constant: TypedValue, + /// Offset position in the list of MIR instructions + mir_offset: usize, + /// Used for variables that create multiple locals on the stack when allocated + /// such as structs and optionals. + multi_value: struct { + /// The index of the first local variable + index: u32, + /// The count of local variables this `WValue` consists of. + /// i.e. an ErrorUnion has a 'count' of 2. + count: u32, + }, +}; + +/// Wasm ops, but without input/output/signedness information +/// Used for `buildOpcode` +const Op = enum { + @"unreachable", + nop, + block, + loop, + @"if", + @"else", + end, + br, + br_if, + br_table, + @"return", + call, + call_indirect, + drop, + select, + local_get, + local_set, + local_tee, + global_get, + global_set, + load, + store, + memory_size, + memory_grow, + @"const", + eqz, + eq, + ne, + lt, + gt, + le, + ge, + clz, + ctz, + popcnt, + add, + sub, + mul, + div, + rem, + @"and", + @"or", + xor, + shl, + shr, + rotl, + rotr, + abs, + neg, + ceil, + floor, + trunc, + nearest, + sqrt, + min, + max, + copysign, + wrap, + convert, + demote, + promote, + reinterpret, + extend, +}; + +/// Contains the settings needed to create an `Opcode` using `buildOpcode`. +/// +/// The fields correspond to the opcode name. Here is an example +/// i32_trunc_f32_s +/// ^ ^ ^ ^ +/// | | | | +/// valtype1 | | | +/// = .i32 | | | +/// | | | +/// op | | +/// = .trunc | | +/// | | +/// valtype2 | +/// = .f32 | +/// | +/// width | +/// = null | +/// | +/// signed +/// = true +/// +/// There can be missing fields, here are some more examples: +/// i64_load8_u +/// --> .{ .valtype1 = .i64, .op = .load, .width = 8, signed = false } +/// i32_mul +/// --> .{ .valtype1 = .i32, .op = .trunc } +/// nop +/// --> .{ .op = .nop } +const OpcodeBuildArguments = struct { + /// First valtype in the opcode (usually represents the type of the output) + valtype1: ?wasm.Valtype = null, + /// The operation (e.g. call, unreachable, div, min, sqrt, etc.) + op: Op, + /// Width of the operation (e.g. 8 for i32_load8_s, 16 for i64_extend16_i32_s) + width: ?u8 = null, + /// Second valtype in the opcode name (usually represents the type of the input) + valtype2: ?wasm.Valtype = null, + /// Signedness of the op + signedness: ?std.builtin.Signedness = null, +}; + +/// Helper function that builds an Opcode given the arguments needed +fn buildOpcode(args: OpcodeBuildArguments) wasm.Opcode { + switch (args.op) { + .@"unreachable" => return .@"unreachable", + .nop => return .nop, + .block => return .block, + .loop => return .loop, + .@"if" => return .@"if", + .@"else" => return .@"else", + .end => return .end, + .br => return .br, + .br_if => return .br_if, + .br_table => return .br_table, + .@"return" => return .@"return", + .call => return .call, + .call_indirect => return .call_indirect, + .drop => return .drop, + .select => return .select, + .local_get => return .local_get, + .local_set => return .local_set, + .local_tee => return .local_tee, + .global_get => return .global_get, + .global_set => return .global_set, + + .load => if (args.width) |width| switch (width) { + 8 => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_load8_s else return .i32_load8_u, + .i64 => if (args.signedness.? == .signed) return .i64_load8_s else return .i64_load8_u, + .f32, .f64 => unreachable, + }, + 16 => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_load16_s else return .i32_load16_u, + .i64 => if (args.signedness.? == .signed) return .i64_load16_s else return .i64_load16_u, + .f32, .f64 => unreachable, + }, + 32 => switch (args.valtype1.?) { + .i64 => if (args.signedness.? == .signed) return .i64_load32_s else return .i64_load32_u, + .i32, .f32, .f64 => unreachable, + }, + else => unreachable, + } else switch (args.valtype1.?) { + .i32 => return .i32_load, + .i64 => return .i64_load, + .f32 => return .f32_load, + .f64 => return .f64_load, + }, + .store => if (args.width) |width| { + switch (width) { + 8 => switch (args.valtype1.?) { + .i32 => return .i32_store8, + .i64 => return .i64_store8, + .f32, .f64 => unreachable, + }, + 16 => switch (args.valtype1.?) { + .i32 => return .i32_store16, + .i64 => return .i64_store16, + .f32, .f64 => unreachable, + }, + 32 => switch (args.valtype1.?) { + .i64 => return .i64_store32, + .i32, .f32, .f64 => unreachable, + }, + else => unreachable, + } + } else { + switch (args.valtype1.?) { + .i32 => return .i32_store, + .i64 => return .i64_store, + .f32 => return .f32_store, + .f64 => return .f64_store, + } + }, + + .memory_size => return .memory_size, + .memory_grow => return .memory_grow, + + .@"const" => switch (args.valtype1.?) { + .i32 => return .i32_const, + .i64 => return .i64_const, + .f32 => return .f32_const, + .f64 => return .f64_const, + }, + + .eqz => switch (args.valtype1.?) { + .i32 => return .i32_eqz, + .i64 => return .i64_eqz, + .f32, .f64 => unreachable, + }, + .eq => switch (args.valtype1.?) { + .i32 => return .i32_eq, + .i64 => return .i64_eq, + .f32 => return .f32_eq, + .f64 => return .f64_eq, + }, + .ne => switch (args.valtype1.?) { + .i32 => return .i32_ne, + .i64 => return .i64_ne, + .f32 => return .f32_ne, + .f64 => return .f64_ne, + }, + + .lt => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_lt_s else return .i32_lt_u, + .i64 => if (args.signedness.? == .signed) return .i64_lt_s else return .i64_lt_u, + .f32 => return .f32_lt, + .f64 => return .f64_lt, + }, + .gt => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_gt_s else return .i32_gt_u, + .i64 => if (args.signedness.? == .signed) return .i64_gt_s else return .i64_gt_u, + .f32 => return .f32_gt, + .f64 => return .f64_gt, + }, + .le => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_le_s else return .i32_le_u, + .i64 => if (args.signedness.? == .signed) return .i64_le_s else return .i64_le_u, + .f32 => return .f32_le, + .f64 => return .f64_le, + }, + .ge => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_ge_s else return .i32_ge_u, + .i64 => if (args.signedness.? == .signed) return .i64_ge_s else return .i64_ge_u, + .f32 => return .f32_ge, + .f64 => return .f64_ge, + }, + + .clz => switch (args.valtype1.?) { + .i32 => return .i32_clz, + .i64 => return .i64_clz, + .f32, .f64 => unreachable, + }, + .ctz => switch (args.valtype1.?) { + .i32 => return .i32_ctz, + .i64 => return .i64_ctz, + .f32, .f64 => unreachable, + }, + .popcnt => switch (args.valtype1.?) { + .i32 => return .i32_popcnt, + .i64 => return .i64_popcnt, + .f32, .f64 => unreachable, + }, + + .add => switch (args.valtype1.?) { + .i32 => return .i32_add, + .i64 => return .i64_add, + .f32 => return .f32_add, + .f64 => return .f64_add, + }, + .sub => switch (args.valtype1.?) { + .i32 => return .i32_sub, + .i64 => return .i64_sub, + .f32 => return .f32_sub, + .f64 => return .f64_sub, + }, + .mul => switch (args.valtype1.?) { + .i32 => return .i32_mul, + .i64 => return .i64_mul, + .f32 => return .f32_mul, + .f64 => return .f64_mul, + }, + + .div => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_div_s else return .i32_div_u, + .i64 => if (args.signedness.? == .signed) return .i64_div_s else return .i64_div_u, + .f32 => return .f32_div, + .f64 => return .f64_div, + }, + .rem => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_rem_s else return .i32_rem_u, + .i64 => if (args.signedness.? == .signed) return .i64_rem_s else return .i64_rem_u, + .f32, .f64 => unreachable, + }, + + .@"and" => switch (args.valtype1.?) { + .i32 => return .i32_and, + .i64 => return .i64_and, + .f32, .f64 => unreachable, + }, + .@"or" => switch (args.valtype1.?) { + .i32 => return .i32_or, + .i64 => return .i64_or, + .f32, .f64 => unreachable, + }, + .xor => switch (args.valtype1.?) { + .i32 => return .i32_xor, + .i64 => return .i64_xor, + .f32, .f64 => unreachable, + }, + + .shl => switch (args.valtype1.?) { + .i32 => return .i32_shl, + .i64 => return .i64_shl, + .f32, .f64 => unreachable, + }, + .shr => switch (args.valtype1.?) { + .i32 => if (args.signedness.? == .signed) return .i32_shr_s else return .i32_shr_u, + .i64 => if (args.signedness.? == .signed) return .i64_shr_s else return .i64_shr_u, + .f32, .f64 => unreachable, + }, + .rotl => switch (args.valtype1.?) { + .i32 => return .i32_rotl, + .i64 => return .i64_rotl, + .f32, .f64 => unreachable, + }, + .rotr => switch (args.valtype1.?) { + .i32 => return .i32_rotr, + .i64 => return .i64_rotr, + .f32, .f64 => unreachable, + }, + + .abs => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_abs, + .f64 => return .f64_abs, + }, + .neg => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_neg, + .f64 => return .f64_neg, + }, + .ceil => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_ceil, + .f64 => return .f64_ceil, + }, + .floor => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_floor, + .f64 => return .f64_floor, + }, + .trunc => switch (args.valtype1.?) { + .i32 => switch (args.valtype2.?) { + .i32 => unreachable, + .i64 => unreachable, + .f32 => if (args.signedness.? == .signed) return .i32_trunc_f32_s else return .i32_trunc_f32_u, + .f64 => if (args.signedness.? == .signed) return .i32_trunc_f64_s else return .i32_trunc_f64_u, + }, + .i64 => unreachable, + .f32 => return .f32_trunc, + .f64 => return .f64_trunc, + }, + .nearest => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_nearest, + .f64 => return .f64_nearest, + }, + .sqrt => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_sqrt, + .f64 => return .f64_sqrt, + }, + .min => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_min, + .f64 => return .f64_min, + }, + .max => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_max, + .f64 => return .f64_max, + }, + .copysign => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => return .f32_copysign, + .f64 => return .f64_copysign, + }, + + .wrap => switch (args.valtype1.?) { + .i32 => switch (args.valtype2.?) { + .i32 => unreachable, + .i64 => return .i32_wrap_i64, + .f32, .f64 => unreachable, + }, + .i64, .f32, .f64 => unreachable, + }, + .convert => switch (args.valtype1.?) { + .i32, .i64 => unreachable, + .f32 => switch (args.valtype2.?) { + .i32 => if (args.signedness.? == .signed) return .f32_convert_i32_s else return .f32_convert_i32_u, + .i64 => if (args.signedness.? == .signed) return .f32_convert_i64_s else return .f32_convert_i64_u, + .f32, .f64 => unreachable, + }, + .f64 => switch (args.valtype2.?) { + .i32 => if (args.signedness.? == .signed) return .f64_convert_i32_s else return .f64_convert_i32_u, + .i64 => if (args.signedness.? == .signed) return .f64_convert_i64_s else return .f64_convert_i64_u, + .f32, .f64 => unreachable, + }, + }, + .demote => if (args.valtype1.? == .f32 and args.valtype2.? == .f64) return .f32_demote_f64 else unreachable, + .promote => if (args.valtype1.? == .f64 and args.valtype2.? == .f32) return .f64_promote_f32 else unreachable, + .reinterpret => switch (args.valtype1.?) { + .i32 => if (args.valtype2.? == .f32) return .i32_reinterpret_f32 else unreachable, + .i64 => if (args.valtype2.? == .f64) return .i64_reinterpret_f64 else unreachable, + .f32 => if (args.valtype2.? == .i32) return .f32_reinterpret_i32 else unreachable, + .f64 => if (args.valtype2.? == .i64) return .f64_reinterpret_i64 else unreachable, + }, + .extend => switch (args.valtype1.?) { + .i32 => switch (args.width.?) { + 8 => if (args.signedness.? == .signed) return .i32_extend8_s else unreachable, + 16 => if (args.signedness.? == .signed) return .i32_extend16_s else unreachable, + else => unreachable, + }, + .i64 => switch (args.width.?) { + 8 => if (args.signedness.? == .signed) return .i64_extend8_s else unreachable, + 16 => if (args.signedness.? == .signed) return .i64_extend16_s else unreachable, + 32 => if (args.signedness.? == .signed) return .i64_extend32_s else unreachable, + else => unreachable, + }, + .f32, .f64 => unreachable, + }, + } +} + +test "Wasm - buildOpcode" { + // Make sure buildOpcode is referenced, and test some examples + const i32_const = buildOpcode(.{ .op = .@"const", .valtype1 = .i32 }); + const end = buildOpcode(.{ .op = .end }); + const local_get = buildOpcode(.{ .op = .local_get }); + const i64_extend32_s = buildOpcode(.{ .op = .extend, .valtype1 = .i64, .width = 32, .signedness = .signed }); + const f64_reinterpret_i64 = buildOpcode(.{ .op = .reinterpret, .valtype1 = .f64, .valtype2 = .i64 }); + + try testing.expectEqual(@as(wasm.Opcode, .i32_const), i32_const); + try testing.expectEqual(@as(wasm.Opcode, .end), end); + try testing.expectEqual(@as(wasm.Opcode, .local_get), local_get); + try testing.expectEqual(@as(wasm.Opcode, .i64_extend32_s), i64_extend32_s); + try testing.expectEqual(@as(wasm.Opcode, .f64_reinterpret_i64), f64_reinterpret_i64); +} + +pub const Result = union(enum) { + /// The codegen bytes have been appended to `Context.code` + appended: void, + /// The data is managed externally and are part of the `Result` + externally_managed: []const u8, +}; + +/// Hashmap to store generated `WValue` for each `Air.Inst.Ref` +pub const ValueTable = std.AutoHashMapUnmanaged(Air.Inst.Index, WValue); + +const Self = @This(); + +/// Reference to the function declaration the code +/// section belongs to +decl: *Decl, +air: Air, +liveness: Liveness, +gpa: *mem.Allocator, +/// Table to save `WValue`'s generated by an `Air.Inst` +values: ValueTable, +/// Mapping from Air.Inst.Index to block ids +blocks: std.AutoArrayHashMapUnmanaged(Air.Inst.Index, u32) = .{}, +/// `bytes` contains the wasm bytecode belonging to the 'code' section. +code: ArrayList(u8), +/// Contains the generated function type bytecode for the current function +/// found in `decl` +func_type_data: ArrayList(u8), +/// The index the next local generated will have +/// NOTE: arguments share the index with locals therefore the first variable +/// will have the index that comes after the last argument's index +local_index: u32 = 0, +/// If codegen fails, an error messages will be allocated and saved in `err_msg` +err_msg: *Module.ErrorMsg, +/// Current block depth. Used to calculate the relative difference between a break +/// and block +block_depth: u32 = 0, +/// List of all locals' types generated throughout this declaration +/// used to emit locals count at start of 'code' section. +locals: std.ArrayListUnmanaged(u8), +/// The Target we're emitting (used to call intInfo) +target: std.Target, +/// Represents the wasm binary file that is being linked. +bin_file: *link.File, +/// Table with the global error set. Consists of every error found in +/// the compiled code. Each error name maps to a `Module.ErrorInt` which is emitted +/// during codegen to determine the error value. +global_error_set: std.StringHashMapUnmanaged(Module.ErrorInt), +/// List of MIR Instructions +mir_instructions: std.MultiArrayList(Mir.Inst) = .{}, +/// Contains extra data for MIR +mir_extra: std.ArrayListUnmanaged(u32) = .{}, + +const InnerError = error{ + OutOfMemory, + /// An error occured when trying to lower AIR to MIR. + CodegenFail, + /// Can occur when dereferencing a pointer that points to a `Decl` of which the analysis has failed + AnalysisFail, + /// Failed to emit MIR instructions to binary/textual representation. + EmitFail, +}; + +pub fn deinit(self: *Self) void { + self.values.deinit(self.gpa); + self.blocks.deinit(self.gpa); + self.locals.deinit(self.gpa); + self.mir_instructions.deinit(self.gpa); + self.mir_extra.deinit(self.gpa); + self.* = undefined; +} + +/// Sets `err_msg` on `Context` and returns `error.CodegemFail` which is caught in link/Wasm.zig +fn fail(self: *Self, comptime fmt: []const u8, args: anytype) InnerError { + const src: LazySrcLoc = .{ .node_offset = 0 }; + const src_loc = src.toSrcLoc(self.decl); + self.err_msg = try Module.ErrorMsg.create(self.gpa, src_loc, fmt, args); + return error.CodegenFail; +} + +/// Resolves the `WValue` for the given instruction `inst` +/// When the given instruction has a `Value`, it returns a constant instead +fn resolveInst(self: Self, ref: Air.Inst.Ref) WValue { + const inst_index = Air.refToIndex(ref) orelse { + const tv = Air.Inst.Ref.typed_value_map[@enumToInt(ref)]; + if (!tv.ty.hasCodeGenBits()) { + return WValue.none; + } + return WValue{ .constant = tv }; + }; + + const inst_type = self.air.typeOfIndex(inst_index); + if (!inst_type.hasCodeGenBits()) return .none; + + if (self.air.instructions.items(.tag)[inst_index] == .constant) { + const ty_pl = self.air.instructions.items(.data)[inst_index].ty_pl; + return WValue{ .constant = .{ .ty = inst_type, .val = self.air.values[ty_pl.payload] } }; + } + + return self.values.get(inst_index).?; // Instruction does not dominate all uses! +} + +/// Appends a MIR instruction and returns its index within the list of instructions +fn addInst(self: *Self, inst: Mir.Inst) error{OutOfMemory}!void { + try self.mir_instructions.append(self.gpa, inst); +} + +/// Inserts a Mir instruction at the given `offset`. +/// Asserts offset is within bound. +fn addInstAt(self: *Self, offset: usize, inst: Mir.Inst) error{OutOfMemory}!void { + try self.mir_instructions.ensureUnusedCapacity(self.gpa, 1); + self.mir_instructions.insertAssumeCapacity(offset, inst); +} + +fn addTag(self: *Self, tag: Mir.Inst.Tag) error{OutOfMemory}!void { + try self.addInst(.{ .tag = tag, .data = .{ .tag = {} } }); +} + +fn addLabel(self: *Self, tag: Mir.Inst.Tag, label: u32) error{OutOfMemory}!void { + try self.addInst(.{ .tag = tag, .data = .{ .label = label } }); +} + +fn addImm32(self: *Self, imm: i32) error{OutOfMemory}!void { + try self.addInst(.{ .tag = .i32_const, .data = .{ .imm32 = imm } }); +} + +/// Accepts an unsigned 64bit integer rather than a signed integer to +/// prevent us from having to bitcast multiple times as most values +/// within codegen are represented as unsigned rather than signed. +fn addImm64(self: *Self, imm: u64) error{OutOfMemory}!void { + const extra_index = try self.addExtra(Mir.Imm64.fromU64(imm)); + try self.addInst(.{ .tag = .i64_const, .data = .{ .payload = extra_index } }); +} + +fn addFloat64(self: *Self, float: f64) error{OutOfMemory}!void { + const extra_index = try self.addExtra(Mir.Float64.fromFloat64(float)); + try self.addInst(.{ .tag = .f64_const, .data = .{ .payload = extra_index } }); +} + +/// Appends entries to `mir_extra` based on the type of `extra`. +/// Returns the index into `mir_extra` +fn addExtra(self: *Self, extra: anytype) error{OutOfMemory}!u32 { + const fields = std.meta.fields(@TypeOf(extra)); + try self.mir_extra.ensureUnusedCapacity(self.gpa, fields.len); + return self.addExtraAssumeCapacity(extra); +} + +/// Appends entries to `mir_extra` based on the type of `extra`. +/// Returns the index into `mir_extra` +fn addExtraAssumeCapacity(self: *Self, extra: anytype) error{OutOfMemory}!u32 { + const fields = std.meta.fields(@TypeOf(extra)); + const result = @intCast(u32, self.mir_extra.items.len); + inline for (fields) |field| { + self.mir_extra.appendAssumeCapacity(switch (field.field_type) { + u32 => @field(extra, field.name), + else => |field_type| @compileError("Unsupported field type " ++ @typeName(field_type)), + }); + } + return result; +} + +/// Using a given `Type`, returns the corresponding wasm Valtype +fn typeToValtype(self: *Self, ty: Type) InnerError!wasm.Valtype { + return switch (ty.zigTypeTag()) { + .Float => blk: { + const bits = ty.floatBits(self.target); + if (bits == 16 or bits == 32) break :blk wasm.Valtype.f32; + if (bits == 64) break :blk wasm.Valtype.f64; + return self.fail("Float bit size not supported by wasm: '{d}'", .{bits}); + }, + .Int => blk: { + const info = ty.intInfo(self.target); + if (info.bits <= 32) break :blk wasm.Valtype.i32; + if (info.bits > 32 and info.bits <= 64) break :blk wasm.Valtype.i64; + return self.fail("Integer bit size not supported by wasm: '{d}'", .{info.bits}); + }, + .Enum => switch (ty.tag()) { + .enum_simple => wasm.Valtype.i32, + else => self.typeToValtype(ty.cast(Type.Payload.EnumFull).?.data.tag_ty), + }, + .Bool, + .Pointer, + .ErrorSet, + => wasm.Valtype.i32, + .Struct, .ErrorUnion, .Optional => unreachable, // Multi typed, must be handled individually. + else => |tag| self.fail("TODO - Wasm valtype for type '{s}'", .{tag}), + }; +} + +/// Using a given `Type`, returns the byte representation of its wasm value type +fn genValtype(self: *Self, ty: Type) InnerError!u8 { + return wasm.valtype(try self.typeToValtype(ty)); +} + +/// Using a given `Type`, returns the corresponding wasm value type +/// Differently from `genValtype` this also allows `void` to create a block +/// with no return type +fn genBlockType(self: *Self, ty: Type) InnerError!u8 { + return switch (ty.tag()) { + .void, .noreturn => wasm.block_empty, + else => self.genValtype(ty), + }; +} + +/// Writes the bytecode depending on the given `WValue` in `val` +fn emitWValue(self: *Self, val: WValue) InnerError!void { + switch (val) { + .multi_value => unreachable, // multi_value can never be written directly, and must be accessed individually + .none, .mir_offset => {}, // no-op + .local => |idx| { + try self.addLabel(.local_get, idx); + }, + .constant => |tv| try self.emitConstant(tv.val, tv.ty), // Creates a new constant on the stack + } +} + +/// Creates one or multiple locals for a given `Type`. +/// Returns a corresponding `Wvalue` that can either be of tag +/// local or multi_value +fn allocLocal(self: *Self, ty: Type) InnerError!WValue { + const initial_index = self.local_index; + switch (ty.zigTypeTag()) { + .Struct => { + // for each struct field, generate a local + const struct_data: *Module.Struct = ty.castTag(.@"struct").?.data; + const fields_len = @intCast(u32, struct_data.fields.count()); + try self.locals.ensureUnusedCapacity(self.gpa, fields_len); + for (struct_data.fields.values()) |*value| { + const val_type = try self.genValtype(value.ty); + self.locals.appendAssumeCapacity(val_type); + self.local_index += 1; + } + return WValue{ .multi_value = .{ + .index = initial_index, + .count = fields_len, + } }; + }, + .ErrorUnion => { + const payload_type = ty.errorUnionPayload(); + const val_type = try self.genValtype(payload_type); + + // we emit the error value as the first local, and the payload as the following. + // The first local is also used to find the index of the error and payload. + // + // TODO: Add support where the payload is a type that contains multiple locals such as a struct. + try self.locals.ensureUnusedCapacity(self.gpa, 2); + self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // error values are always i32 + self.locals.appendAssumeCapacity(val_type); + self.local_index += 2; + + return WValue{ .multi_value = .{ + .index = initial_index, + .count = 2, + } }; + }, + .Optional => { + var opt_buf: Type.Payload.ElemType = undefined; + const child_type = ty.optionalChild(&opt_buf); + if (ty.isPtrLikeOptional()) { + return self.fail("TODO: wasm optional pointer", .{}); + } + + try self.locals.ensureUnusedCapacity(self.gpa, 2); + self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // optional 'tag' for null-checking is always i32 + self.locals.appendAssumeCapacity(try self.genValtype(child_type)); + self.local_index += 2; + + return WValue{ .multi_value = .{ + .index = initial_index, + .count = 2, + } }; + }, + else => { + const valtype = try self.genValtype(ty); + try self.locals.append(self.gpa, valtype); + self.local_index += 1; + return WValue{ .local = initial_index }; + }, + } +} + +fn genFunctype(self: *Self) InnerError!void { + assert(self.decl.has_tv); + const ty = self.decl.ty; + const writer = self.func_type_data.writer(); + + try writer.writeByte(wasm.function_type); + + // param types + try leb.writeULEB128(writer, @intCast(u32, ty.fnParamLen())); + if (ty.fnParamLen() != 0) { + const params = try self.gpa.alloc(Type, ty.fnParamLen()); + defer self.gpa.free(params); + ty.fnParamTypes(params); + for (params) |param_type| { + // Can we maybe get the source index of each param? + const val_type = try self.genValtype(param_type); + try writer.writeByte(val_type); + } + } + + // return type + const return_type = ty.fnReturnType(); + switch (return_type.zigTypeTag()) { + .Void, .NoReturn => try leb.writeULEB128(writer, @as(u32, 0)), + .Struct => return self.fail("TODO: Implement struct as return type for wasm", .{}), + .Optional => return self.fail("TODO: Implement optionals as return type for wasm", .{}), + .ErrorUnion => { + const val_type = try self.genValtype(return_type.errorUnionPayload()); + + // write down the amount of return values + try leb.writeULEB128(writer, @as(u32, 2)); + try writer.writeByte(wasm.valtype(.i32)); // error code is always an i32 integer. + try writer.writeByte(val_type); + }, + else => { + try leb.writeULEB128(writer, @as(u32, 1)); + // Can we maybe get the source index of the return type? + const val_type = try self.genValtype(return_type); + try writer.writeByte(val_type); + }, + } +} + +pub fn genFunc(self: *Self) InnerError!Result { + try self.genFunctype(); + // TODO: check for and handle death of instructions + + // Generate MIR for function body + try self.genBody(self.air.getMainBody()); + // End of function body + try self.addTag(.end); + + var mir: Mir = .{ + .instructions = self.mir_instructions.toOwnedSlice(), + .extra = self.mir_extra.toOwnedSlice(self.gpa), + }; + defer mir.deinit(self.gpa); + + var emit: Emit = .{ + .mir = mir, + .bin_file = self.bin_file, + .code = &self.code, + .locals = self.locals.items, + .decl = self.decl, + }; + + emit.emitMir() catch |err| switch (err) { + error.EmitFail => { + self.err_msg = emit.error_msg.?; + return error.EmitFail; + }, + else => |e| return e, + }; + + // codegen data has been appended to `code` + return Result.appended; +} + +/// Generates the wasm bytecode for the declaration belonging to `Context` +pub fn gen(self: *Self, ty: Type, val: Value) InnerError!Result { + switch (ty.zigTypeTag()) { + .Fn => { + try self.genFunctype(); + if (val.tag() == .extern_fn) { + return Result.appended; // don't need code body for extern functions + } + return self.fail("TODO implement wasm codegen for function pointers", .{}); + }, + .Array => { + if (val.castTag(.bytes)) |payload| { + if (ty.sentinel()) |sentinel| { + try self.code.appendSlice(payload.data); + + switch (try self.gen(ty.elemType(), sentinel)) { + .appended => return Result.appended, + .externally_managed => |data| { + try self.code.appendSlice(data); + return Result.appended; + }, + } + } + return Result{ .externally_managed = payload.data }; + } else return self.fail("TODO implement gen for more kinds of arrays", .{}); + }, + .Int => { + const info = ty.intInfo(self.target); + if (info.bits == 8 and info.signedness == .unsigned) { + const int_byte = val.toUnsignedInt(); + try self.code.append(@intCast(u8, int_byte)); + return Result.appended; + } + return self.fail("TODO: Implement codegen for int type: '{}'", .{ty}); + }, + .Enum => { + try self.emitConstant(val, ty); + return Result.appended; + }, + .Struct => { + // TODO write the fields for real + try self.code.writer().writeByteNTimes(0xaa, ty.abiSize(self.target)); + return Result{ .appended = {} }; + }, + else => |tag| return self.fail("TODO: Implement zig type codegen for type: '{s}'", .{tag}), + } +} + +fn genInst(self: *Self, inst: Air.Inst.Index) !WValue { + const air_tags = self.air.instructions.items(.tag); + return switch (air_tags[inst]) { + .add => self.airBinOp(inst, .add), + .addwrap => self.airWrapBinOp(inst, .add), + .sub => self.airBinOp(inst, .sub), + .subwrap => self.airWrapBinOp(inst, .sub), + .mul => self.airBinOp(inst, .mul), + .mulwrap => self.airWrapBinOp(inst, .mul), + .div_trunc => self.airBinOp(inst, .div), + .bit_and => self.airBinOp(inst, .@"and"), + .bit_or => self.airBinOp(inst, .@"or"), + .bool_and => self.airBinOp(inst, .@"and"), + .bool_or => self.airBinOp(inst, .@"or"), + .xor => self.airBinOp(inst, .xor), + + .cmp_eq => self.airCmp(inst, .eq), + .cmp_gte => self.airCmp(inst, .gte), + .cmp_gt => self.airCmp(inst, .gt), + .cmp_lte => self.airCmp(inst, .lte), + .cmp_lt => self.airCmp(inst, .lt), + .cmp_neq => self.airCmp(inst, .neq), + + .alloc => self.airAlloc(inst), + .arg => self.airArg(inst), + .bitcast => self.airBitcast(inst), + .block => self.airBlock(inst), + .breakpoint => self.airBreakpoint(inst), + .br => self.airBr(inst), + .call => self.airCall(inst), + .cond_br => self.airCondBr(inst), + .constant => unreachable, + .dbg_stmt => WValue.none, + .intcast => self.airIntcast(inst), + + .is_err => self.airIsErr(inst, .i32_ne), + .is_non_err => self.airIsErr(inst, .i32_eq), + + .is_null => self.airIsNull(inst, .i32_ne), + .is_non_null => self.airIsNull(inst, .i32_eq), + .is_null_ptr => self.airIsNull(inst, .i32_ne), + .is_non_null_ptr => self.airIsNull(inst, .i32_eq), + + .load => self.airLoad(inst), + .loop => self.airLoop(inst), + .not => self.airNot(inst), + .ret => self.airRet(inst), + .store => self.airStore(inst), + .struct_field_ptr => self.airStructFieldPtr(inst), + .struct_field_ptr_index_0 => self.airStructFieldPtrIndex(inst, 0), + .struct_field_ptr_index_1 => self.airStructFieldPtrIndex(inst, 1), + .struct_field_ptr_index_2 => self.airStructFieldPtrIndex(inst, 2), + .struct_field_ptr_index_3 => self.airStructFieldPtrIndex(inst, 3), + .struct_field_val => self.airStructFieldVal(inst), + .switch_br => self.airSwitchBr(inst), + .unreach => self.airUnreachable(inst), + .wrap_optional => self.airWrapOptional(inst), + + .unwrap_errunion_payload => self.airUnwrapErrUnionPayload(inst), + .wrap_errunion_payload => self.airWrapErrUnionPayload(inst), + + .optional_payload => self.airOptionalPayload(inst), + .optional_payload_ptr => self.airOptionalPayload(inst), + .optional_payload_ptr_set => self.airOptionalPayloadPtrSet(inst), + else => |tag| self.fail("TODO: Implement wasm inst: {s}", .{@tagName(tag)}), + }; +} + +fn genBody(self: *Self, body: []const Air.Inst.Index) InnerError!void { + for (body) |inst| { + const result = try self.genInst(inst); + try self.values.putNoClobber(self.gpa, inst, result); + } +} + +fn airRet(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const un_op = self.air.instructions.items(.data)[inst].un_op; + const operand = self.resolveInst(un_op); + try self.emitWValue(operand); + try self.addTag(.@"return"); + return .none; +} + +fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const extra = self.air.extraData(Air.Call, pl_op.payload); + const args = self.air.extra[extra.end..][0..extra.data.args_len]; + + const target: *Decl = blk: { + const func_val = self.air.value(pl_op.operand).?; + + if (func_val.castTag(.function)) |func| { + break :blk func.data.owner_decl; + } else if (func_val.castTag(.extern_fn)) |ext_fn| { + break :blk ext_fn.data; + } + return self.fail("Expected a function, but instead found type '{s}'", .{func_val.tag()}); + }; + + for (args) |arg| { + const arg_val = self.resolveInst(@intToEnum(Air.Inst.Ref, arg)); + try self.emitWValue(arg_val); + } + + try self.addLabel(.call, target.link.wasm.symbol_index); + + return .none; +} + +fn airAlloc(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const elem_type = self.air.typeOfIndex(inst).elemType(); + return self.allocLocal(elem_type); +} + +fn airStore(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + + const lhs = self.resolveInst(bin_op.lhs); + const rhs = self.resolveInst(bin_op.rhs); + + switch (lhs) { + .multi_value => |multi_value| switch (rhs) { + // When assigning a value to a multi_value such as a struct, + // we simply assign the local_index to the rhs one. + // This allows us to update struct fields without having to individually + // set each local as each field's index will be calculated off the struct's base index + .multi_value => self.values.put(self.gpa, Air.refToIndex(bin_op.lhs).?, rhs) catch unreachable, // Instruction does not dominate all uses! + .constant, .none => { + // emit all values onto the stack if constant + try self.emitWValue(rhs); + + // for each local, pop the stack value into the local + // As the last element is on top of the stack, we must populate the locals + // in reverse. + var i: u32 = multi_value.count; + while (i > 0) : (i -= 1) { + try self.addLabel(.local_set, multi_value.index + i - 1); + } + }, + .local => { + // This can occur when we wrap a single value into a multi-value, + // such as wrapping a non-optional value into an optional. + // This means we must zero the null-tag, and set the payload. + assert(multi_value.count == 2); + // set payload + try self.emitWValue(rhs); + try self.addLabel(.local_set, multi_value.index + 1); + }, + else => unreachable, + }, + .local => |local| { + try self.emitWValue(rhs); + try self.addLabel(.local_set, local); + }, + else => unreachable, + } + return .none; +} + +fn airLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + return self.resolveInst(ty_op.operand); +} + +fn airArg(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + _ = inst; + // arguments share the index with locals + defer self.local_index += 1; + return WValue{ .local = self.local_index }; +} + +fn airBinOp(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const lhs = self.resolveInst(bin_op.lhs); + const rhs = self.resolveInst(bin_op.rhs); + + // it's possible for both lhs and/or rhs to return an offset as well, + // in which case we return the first offset occurrence we find. + const offset = blk: { + if (lhs == .mir_offset) break :blk lhs.mir_offset; + if (rhs == .mir_offset) break :blk rhs.mir_offset; + break :blk self.mir_instructions.len; + }; + + try self.emitWValue(lhs); + try self.emitWValue(rhs); + + const bin_ty = self.air.typeOf(bin_op.lhs); + const opcode: wasm.Opcode = buildOpcode(.{ + .op = op, + .valtype1 = try self.typeToValtype(bin_ty), + .signedness = if (bin_ty.isSignedInt()) .signed else .unsigned, + }); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + return WValue{ .mir_offset = offset }; +} + +fn airWrapBinOp(self: *Self, inst: Air.Inst.Index, op: Op) InnerError!WValue { + const bin_op = self.air.instructions.items(.data)[inst].bin_op; + const lhs = self.resolveInst(bin_op.lhs); + const rhs = self.resolveInst(bin_op.rhs); + + // it's possible for both lhs and/or rhs to return an offset as well, + // in which case we return the first offset occurrence we find. + const offset = blk: { + if (lhs == .mir_offset) break :blk lhs.mir_offset; + if (rhs == .mir_offset) break :blk rhs.mir_offset; + break :blk self.mir_instructions.len; + }; + + try self.emitWValue(lhs); + try self.emitWValue(rhs); + + const bin_ty = self.air.typeOf(bin_op.lhs); + const opcode: wasm.Opcode = buildOpcode(.{ + .op = op, + .valtype1 = try self.typeToValtype(bin_ty), + .signedness = if (bin_ty.isSignedInt()) .signed else .unsigned, + }); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + + const int_info = bin_ty.intInfo(self.target); + const bitsize = int_info.bits; + const is_signed = int_info.signedness == .signed; + // if target type bitsize is x < 32 and 32 > x < 64, we perform + // result & ((1< 64) { + return self.fail("TODO wasm: Integer wrapping for bitsizes larger than 64", .{}); + } + + return WValue{ .mir_offset = offset }; +} + +fn emitConstant(self: *Self, val: Value, ty: Type) InnerError!void { + switch (ty.zigTypeTag()) { + .Int => { + const int_info = ty.intInfo(self.target); + // write constant + switch (int_info.signedness) { + .signed => switch (int_info.bits) { + 0...32 => try self.addImm32(@intCast(i32, val.toSignedInt())), + 33...64 => try self.addImm64(@bitCast(u64, val.toSignedInt())), + else => |bits| return self.fail("Wasm todo: emitConstant for integer with {d} bits", .{bits}), + }, + .unsigned => switch (int_info.bits) { + 0...32 => try self.addImm32(@bitCast(i32, @intCast(u32, val.toUnsignedInt()))), + 33...64 => try self.addImm64(val.toUnsignedInt()), + else => |bits| return self.fail("Wasm TODO: emitConstant for integer with {d} bits", .{bits}), + }, + } + }, + .Bool => try self.addImm32(@intCast(i32, val.toSignedInt())), + .Float => { + // write constant + switch (ty.floatBits(self.target)) { + 0...32 => try self.addInst(.{ .tag = .f32_const, .data = .{ .float32 = val.toFloat(f32) } }), + 64 => try self.addFloat64(val.toFloat(f64)), + else => |bits| return self.fail("Wasm TODO: emitConstant for float with {d} bits", .{bits}), + } + }, + .Pointer => { + if (val.castTag(.decl_ref)) |payload| { + const decl = payload.data; + decl.alive = true; + + // offset into the offset table within the 'data' section + const ptr_width = self.target.cpu.arch.ptrBitWidth() / 8; + try self.addImm32(@bitCast(i32, decl.link.wasm.offset_index * ptr_width)); + + // memory instruction followed by their memarg immediate + // memarg ::== x:u32, y:u32 => {align x, offset y} + const extra_index = try self.addExtra(Mir.MemArg{ .offset = 0, .alignment = 0 }); + try self.addInst(.{ .tag = .i32_load, .data = .{ .payload = extra_index } }); + } else return self.fail("Wasm TODO: emitConstant for other const pointer tag {s}", .{val.tag()}); + }, + .Void => {}, + .Enum => { + if (val.castTag(.enum_field_index)) |field_index| { + switch (ty.tag()) { + .enum_simple => try self.addImm32(@bitCast(i32, field_index.data)), + .enum_full, .enum_nonexhaustive => { + const enum_full = ty.cast(Type.Payload.EnumFull).?.data; + if (enum_full.values.count() != 0) { + const tag_val = enum_full.values.keys()[field_index.data]; + try self.emitConstant(tag_val, enum_full.tag_ty); + } else { + try self.addImm32(@bitCast(i32, field_index.data)); + } + }, + else => unreachable, + } + } else { + var int_tag_buffer: Type.Payload.Bits = undefined; + const int_tag_ty = ty.intTagType(&int_tag_buffer); + try self.emitConstant(val, int_tag_ty); + } + }, + .ErrorSet => { + const error_index = self.global_error_set.get(val.getError().?).?; + try self.addImm32(@bitCast(i32, error_index)); + }, + .ErrorUnion => { + const error_type = ty.errorUnionSet(); + const payload_type = ty.errorUnionPayload(); + if (val.castTag(.eu_payload)) |pl| { + const payload_val = pl.data; + // no error, so write a '0' const + try self.addImm32(0); + // after the error code, we emit the payload + try self.emitConstant(payload_val, payload_type); + } else { + // write the error val + try self.emitConstant(val, error_type); + + // no payload, so write a '0' const + try self.addImm32(0); + } + }, + .Optional => { + var buf: Type.Payload.ElemType = undefined; + const payload_type = ty.optionalChild(&buf); + if (ty.isPtrLikeOptional()) { + return self.fail("Wasm TODO: emitConstant for optional pointer", .{}); + } + + // When constant has value 'null', set is_null local to '1' + // and payload to '0' + if (val.castTag(.opt_payload)) |pl| { + const payload_val = pl.data; + try self.addImm32(0); + try self.emitConstant(payload_val, payload_type); + } else { + // set null-tag + try self.addImm32(1); + // null-tag is set, so write a '0' const + try self.addImm32(0); + } + }, + else => |zig_type| return self.fail("Wasm TODO: emitConstant for zigTypeTag {s}", .{zig_type}), + } +} + +/// Returns a `Value` as a signed 32 bit value. +/// It's illegal to provide a value with a type that cannot be represented +/// as an integer value. +fn valueAsI32(self: Self, val: Value, ty: Type) i32 { + switch (ty.zigTypeTag()) { + .Enum => { + if (val.castTag(.enum_field_index)) |field_index| { + switch (ty.tag()) { + .enum_simple => return @bitCast(i32, field_index.data), + .enum_full, .enum_nonexhaustive => { + const enum_full = ty.cast(Type.Payload.EnumFull).?.data; + if (enum_full.values.count() != 0) { + const tag_val = enum_full.values.keys()[field_index.data]; + return self.valueAsI32(tag_val, enum_full.tag_ty); + } else return @bitCast(i32, field_index.data); + }, + else => unreachable, + } + } else { + var int_tag_buffer: Type.Payload.Bits = undefined; + const int_tag_ty = ty.intTagType(&int_tag_buffer); + return self.valueAsI32(val, int_tag_ty); + } + }, + .Int => switch (ty.intInfo(self.target).signedness) { + .signed => return @truncate(i32, val.toSignedInt()), + .unsigned => return @bitCast(i32, @truncate(u32, val.toUnsignedInt())), + }, + .ErrorSet => { + const error_index = self.global_error_set.get(val.getError().?).?; + return @bitCast(i32, error_index); + }, + else => unreachable, // Programmer called this function for an illegal type + } +} + +fn airBlock(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const block_ty = try self.genBlockType(self.air.getRefType(ty_pl.ty)); + const extra = self.air.extraData(Air.Block, ty_pl.payload); + const body = self.air.extra[extra.end..][0..extra.data.body_len]; + + try self.startBlock(.block, block_ty, null); + // Here we set the current block idx, so breaks know the depth to jump + // to when breaking out. + try self.blocks.putNoClobber(self.gpa, inst, self.block_depth); + try self.genBody(body); + try self.endBlock(); + + return .none; +} + +/// appends a new wasm block to the code section and increases the `block_depth` by 1 +fn startBlock(self: *Self, block_tag: wasm.Opcode, valtype: u8, with_offset: ?usize) !void { + self.block_depth += 1; + const offset = with_offset orelse self.mir_instructions.len; + try self.addInstAt(offset, .{ + .tag = Mir.Inst.Tag.fromOpcode(block_tag), + .data = .{ .block_type = valtype }, + }); +} + +/// Ends the current wasm block and decreases the `block_depth` by 1 +fn endBlock(self: *Self) !void { + try self.addTag(.end); + self.block_depth -= 1; +} + +fn airLoop(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const loop = self.air.extraData(Air.Block, ty_pl.payload); + const body = self.air.extra[loop.end..][0..loop.data.body_len]; + + // result type of loop is always 'noreturn', meaning we can always + // emit the wasm type 'block_empty'. + try self.startBlock(.loop, wasm.block_empty, null); + try self.genBody(body); + + // breaking to the index of a loop block will continue the loop instead + try self.addLabel(.br, 0); + try self.endBlock(); + + return .none; +} + +fn airCondBr(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const condition = self.resolveInst(pl_op.operand); + const extra = self.air.extraData(Air.CondBr, pl_op.payload); + const then_body = self.air.extra[extra.end..][0..extra.data.then_body_len]; + const else_body = self.air.extra[extra.end + then_body.len ..][0..extra.data.else_body_len]; + // TODO: Handle death instructions for then and else body + + // insert blocks at the position of `offset` so + // the condition can jump to it + const offset = switch (condition) { + .mir_offset => |offset| offset, + else => blk: { + const offset = self.mir_instructions.len; + try self.emitWValue(condition); + break :blk offset; + }, + }; + + // result type is always noreturn, so use `block_empty` as type. + try self.startBlock(.block, wasm.block_empty, offset); + + // we inserted the block in front of the condition + // so now check if condition matches. If not, break outside this block + // and continue with the then codepath + try self.addLabel(.br_if, 0); + + try self.genBody(else_body); + try self.endBlock(); + + // Outer block that matches the condition + try self.genBody(then_body); + + return .none; +} + +fn airCmp(self: *Self, inst: Air.Inst.Index, op: std.math.CompareOperator) InnerError!WValue { + // save offset, so potential conditions can insert blocks in front of + // the comparison that we can later jump back to + const offset = self.mir_instructions.len; + + const data: Air.Inst.Data = self.air.instructions.items(.data)[inst]; + const lhs = self.resolveInst(data.bin_op.lhs); + const rhs = self.resolveInst(data.bin_op.rhs); + const lhs_ty = self.air.typeOf(data.bin_op.lhs); + + try self.emitWValue(lhs); + try self.emitWValue(rhs); + + const signedness: std.builtin.Signedness = blk: { + // by default we tell the operand type is unsigned (i.e. bools and enum values) + if (lhs_ty.zigTypeTag() != .Int) break :blk .unsigned; + + // incase of an actual integer, we emit the correct signedness + break :blk lhs_ty.intInfo(self.target).signedness; + }; + const opcode: wasm.Opcode = buildOpcode(.{ + .valtype1 = try self.typeToValtype(lhs_ty), + .op = switch (op) { + .lt => .lt, + .lte => .le, + .eq => .eq, + .neq => .ne, + .gte => .ge, + .gt => .gt, + }, + .signedness = signedness, + }); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + return WValue{ .mir_offset = offset }; +} + +fn airBr(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const br = self.air.instructions.items(.data)[inst].br; + + // if operand has codegen bits we should break with a value + if (self.air.typeOf(br.operand).hasCodeGenBits()) { + try self.emitWValue(self.resolveInst(br.operand)); + } + + // We map every block to its block index. + // We then determine how far we have to jump to it by subtracting it from current block depth + const idx: u32 = self.block_depth - self.blocks.get(br.block_inst).?; + try self.addLabel(.br, idx); + + return .none; +} + +fn airNot(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const offset = self.mir_instructions.len; + + const operand = self.resolveInst(ty_op.operand); + try self.emitWValue(operand); + + // wasm does not have booleans nor the `not` instruction, therefore compare with 0 + // to create the same logic + try self.addImm32(0); + try self.addTag(.i32_eq); + + return WValue{ .mir_offset = offset }; +} + +fn airBreakpoint(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + _ = self; + _ = inst; + // unsupported by wasm itself. Can be implemented once we support DWARF + // for wasm + return .none; +} + +fn airUnreachable(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + _ = inst; + try self.addTag(.@"unreachable"); + return .none; +} + +fn airBitcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + return self.resolveInst(ty_op.operand); +} + +fn airStructFieldPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.StructField, ty_pl.payload); + const struct_ptr = self.resolveInst(extra.data.struct_operand); + return structFieldPtr(struct_ptr, extra.data.field_index); +} +fn airStructFieldPtrIndex(self: *Self, inst: Air.Inst.Index, index: u32) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const struct_ptr = self.resolveInst(ty_op.operand); + return structFieldPtr(struct_ptr, index); +} +fn structFieldPtr(struct_ptr: WValue, index: u32) InnerError!WValue { + return WValue{ .local = struct_ptr.multi_value.index + index }; +} + +fn airStructFieldVal(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + if (self.liveness.isUnused(inst)) return WValue.none; + + const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; + const extra = self.air.extraData(Air.StructField, ty_pl.payload).data; + const struct_multivalue = self.resolveInst(extra.struct_operand).multi_value; + return WValue{ .local = struct_multivalue.index + extra.field_index }; +} + +fn airSwitchBr(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + // result type is always 'noreturn' + const blocktype = wasm.block_empty; + const pl_op = self.air.instructions.items(.data)[inst].pl_op; + const target = self.resolveInst(pl_op.operand); + const target_ty = self.air.typeOf(pl_op.operand); + const switch_br = self.air.extraData(Air.SwitchBr, pl_op.payload); + var extra_index: usize = switch_br.end; + var case_i: u32 = 0; + + // a list that maps each value with its value and body based on the order inside the list. + const CaseValue = struct { integer: i32, value: Value }; + var case_list = try std.ArrayList(struct { + values: []const CaseValue, + body: []const Air.Inst.Index, + }).initCapacity(self.gpa, switch_br.data.cases_len); + defer for (case_list.items) |case| { + self.gpa.free(case.values); + } else case_list.deinit(); + + var lowest: i32 = 0; + var highest: i32 = 0; + while (case_i < switch_br.data.cases_len) : (case_i += 1) { + const case = self.air.extraData(Air.SwitchBr.Case, extra_index); + const items = @bitCast([]const Air.Inst.Ref, self.air.extra[case.end..][0..case.data.items_len]); + const case_body = self.air.extra[case.end + items.len ..][0..case.data.body_len]; + extra_index = case.end + items.len + case_body.len; + const values = try self.gpa.alloc(CaseValue, items.len); + errdefer self.gpa.free(values); + + for (items) |ref, i| { + const item_val = self.air.value(ref).?; + const int_val = self.valueAsI32(item_val, target_ty); + if (int_val < lowest) { + lowest = int_val; + } + if (int_val > highest) { + highest = int_val; + } + values[i] = .{ .integer = int_val, .value = item_val }; + } + + case_list.appendAssumeCapacity(.{ .values = values, .body = case_body }); + try self.startBlock(.block, blocktype, null); + } + + // When the highest and lowest values are seperated by '50', + // we define it as sparse and use an if/else-chain, rather than a jump table. + // When the target is an integer size larger than u32, we have no way to use the value + // as an index, therefore we also use an if/else-chain for those cases. + // TODO: Benchmark this to find a proper value, LLVM seems to draw the line at '40~45'. + const is_sparse = highest - lowest > 50 or target_ty.bitSize(self.target) > 32; + + const else_body = self.air.extra[extra_index..][0..switch_br.data.else_body_len]; + const has_else_body = else_body.len != 0; + if (has_else_body) { + try self.startBlock(.block, blocktype, null); + } + + if (!is_sparse) { + // Generate the jump table 'br_table' when the prongs are not sparse. + // The value 'target' represents the index into the table. + // Each index in the table represents a label to the branch + // to jump to. + try self.startBlock(.block, blocktype, null); + try self.emitWValue(target); + if (lowest < 0) { + // since br_table works using indexes, starting from '0', we must ensure all values + // we put inside, are atleast 0. + try self.addImm32(lowest * -1); + try self.addTag(.i32_add); + } + + // Account for default branch so always add '1' + const depth = @intCast(u32, highest - lowest + @boolToInt(has_else_body)) + 1; + const jump_table: Mir.JumpTable = .{ .length = depth }; + const table_extra_index = try self.addExtra(jump_table); + try self.addInst(.{ .tag = .br_table, .data = .{ .payload = table_extra_index } }); + try self.mir_extra.ensureUnusedCapacity(self.gpa, depth); + while (lowest <= highest) : (lowest += 1) { + // idx represents the branch we jump to + const idx = blk: { + for (case_list.items) |case, idx| { + for (case.values) |case_value| { + if (case_value.integer == lowest) break :blk @intCast(u32, idx); + } + } + break :blk if (has_else_body) case_i else unreachable; + }; + self.mir_extra.appendAssumeCapacity(idx); + } else if (has_else_body) { + self.mir_extra.appendAssumeCapacity(case_i); // default branch + } + try self.endBlock(); + } + + const signedness: std.builtin.Signedness = blk: { + // by default we tell the operand type is unsigned (i.e. bools and enum values) + if (target_ty.zigTypeTag() != .Int) break :blk .unsigned; + + // incase of an actual integer, we emit the correct signedness + break :blk target_ty.intInfo(self.target).signedness; + }; + + for (case_list.items) |case| { + // when sparse, we use if/else-chain, so emit conditional checks + if (is_sparse) { + // for single value prong we can emit a simple if + if (case.values.len == 1) { + try self.emitWValue(target); + try self.emitConstant(case.values[0].value, target_ty); + const opcode = buildOpcode(.{ + .valtype1 = try self.typeToValtype(target_ty), + .op = .ne, // not equal, because we want to jump out of this block if it does not match the condition. + .signedness = signedness, + }); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + try self.addLabel(.br_if, 0); + } else { + // in multi-value prongs we must check if any prongs match the target value. + try self.startBlock(.block, blocktype, null); + for (case.values) |value| { + try self.emitWValue(target); + try self.emitConstant(value.value, target_ty); + const opcode = buildOpcode(.{ + .valtype1 = try self.typeToValtype(target_ty), + .op = .eq, + .signedness = signedness, + }); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + try self.addLabel(.br_if, 0); + } + // value did not match any of the prong values + try self.addLabel(.br, 1); + try self.endBlock(); + } + } + try self.genBody(case.body); + try self.endBlock(); + } + + if (has_else_body) { + try self.genBody(else_body); + try self.endBlock(); + } + return .none; +} + +fn airIsErr(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue { + const un_op = self.air.instructions.items(.data)[inst].un_op; + const operand = self.resolveInst(un_op); + const offset = self.mir_instructions.len; + + // load the error value which is positioned at multi_value's index + try self.emitWValue(.{ .local = operand.multi_value.index }); + // Compare the error value with '0' + try self.addImm32(0); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + + return WValue{ .mir_offset = offset }; +} + +fn airUnwrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const operand = self.resolveInst(ty_op.operand); + // The index of multi_value contains the error code. To get the initial index of the payload we get + // the following index. Next, convert it to a `WValue.local` + // + // TODO: Check if payload is a type that requires a multi_value as well and emit that instead. i.e. a struct. + return WValue{ .local = operand.multi_value.index + 1 }; +} + +fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + return self.resolveInst(ty_op.operand); +} + +fn airIntcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const ty = self.air.getRefType(ty_op.ty); + const operand = self.resolveInst(ty_op.operand); + const ref_ty = self.air.typeOf(ty_op.operand); + const ref_info = ref_ty.intInfo(self.target); + const op_bits = ref_info.bits; + const wanted_bits = ty.intInfo(self.target).bits; + + try self.emitWValue(operand); + if (op_bits > 32 and wanted_bits <= 32) { + try self.addTag(.i32_wrap_i64); + } else if (op_bits <= 32 and wanted_bits > 32) { + try self.addTag(switch (ref_info.signedness) { + .signed => .i64_extend_i32_s, + .unsigned => .i64_extend_i32_u, + }); + } + + // other cases are no-op + return .none; +} + +fn airIsNull(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue { + const un_op = self.air.instructions.items(.data)[inst].un_op; + const operand = self.resolveInst(un_op); + + // load the null value which is positioned at multi_value's index + try self.emitWValue(.{ .local = operand.multi_value.index }); + try self.addImm32(0); + try self.addTag(Mir.Inst.Tag.fromOpcode(opcode)); + + // we save the result in a new local + const local = try self.allocLocal(Type.initTag(.i32)); + try self.addLabel(.local_set, local.local); + + return local; +} + +fn airOptionalPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const operand = self.resolveInst(ty_op.operand); + return WValue{ .local = operand.multi_value.index + 1 }; +} + +fn airOptionalPayloadPtrSet(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + const operand = self.resolveInst(ty_op.operand); + _ = operand; + return self.fail("TODO - wasm codegen for optional_payload_ptr_set", .{}); +} + +fn airWrapOptional(self: *Self, inst: Air.Inst.Index) InnerError!WValue { + const ty_op = self.air.instructions.items(.data)[inst].ty_op; + return self.resolveInst(ty_op.operand); +} diff --git a/src/arch/wasm/Emit.zig b/src/arch/wasm/Emit.zig new file mode 100644 index 0000000000..0b09a889f3 --- /dev/null +++ b/src/arch/wasm/Emit.zig @@ -0,0 +1,251 @@ +//! 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 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, + +const InnerError = error{ + OutOfMemory, + EmitFail, +}; + +pub fn emitMir(emit: *Emit) InnerError!void { + const mir_tags = emit.mir.instructions.items(.tag); + // Reserve space to write the size after generating the code. + try emit.code.resize(5); + // 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), + + // 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), + .global_get => try emit.emitGlobal(tag, inst), + .global_set => try emit.emitGlobal(tag, 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), + .i32_store => try emit.emitMemArg(tag, inst), + + .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), + + // no-ops + .end => try emit.emitTag(tag), + .memory_size => try emit.emitTag(tag), + .@"return" => try emit.emitTag(tag), + .@"unreachable" => 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), + .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), + } + } + + // Fill in the size of the generated code to the reserved space at the + // beginning of the buffer. + const size = emit.code.items.len - 5; + leb128.writeUnsignedFixed(5, emit.code.items[0..5], @intCast(u32, size)); +} + +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); + try emit.code.appendSlice(&buf); + + // TODO: Append label to the relocation list of this function +} + +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.writeULEB128(emit.code.writer(), 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)); + try leb128.writeULEB128(emit.code.writer(), mem_arg.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 offset = @intCast(u32, emit.code.items.len); + var buf: [5]u8 = undefined; + leb128.writeUnsignedFixed(5, &buf, label); + try emit.code.appendSlice(&buf); + + // The function index immediate argument will be filled in using this data + // in link.Wasm.flush(). + // TODO: Replace this with proper relocations saved in the Atom. + try emit.decl.fn_link.wasm.idx_refs.append(emit.bin_file.allocator, .{ + .offset = offset, + .decl = label, + }); +} diff --git a/src/arch/wasm/Mir.zig b/src/arch/wasm/Mir.zig new file mode 100644 index 0000000000..5e20b64d39 --- /dev/null +++ b/src/arch/wasm/Mir.zig @@ -0,0 +1,367 @@ +//! Machine Intermediate Representation. +//! This representation is produced by wasm Codegen. +//! Each of these instructions have a 1:1 mapping to a wasm opcode, +//! but may contain metadata for a specific opcode such as an immediate. +//! MIR can be lowered to both textual code (wat) and binary format (wasm). +//! The main benefits of MIR is optimization passes, pre-allocated locals, +//! and known jump labels for blocks. + +const Mir = @This(); + +const std = @import("std"); + +/// A struct of array that represents each individual wasm +instructions: std.MultiArrayList(Inst).Slice, +/// A slice of indexes where the meaning of the data is determined by the +/// `Inst.Tag` value. +extra: []const u32, + +pub const Inst = struct { + /// The opcode that represents this instruction + tag: Tag, + /// Data is determined by the set `tag`. + /// For example, `data` will be an i32 for when `tag` is 'i32_const'. + data: Data, + + /// The position of a given MIR isntruction with the instruction list. + pub const Index = u32; + + /// Contains all possible wasm opcodes the Zig compiler may emit + /// Rather than re-using std.wasm.Opcode, we only declare the opcodes + /// we need, and also use this possibility to document how to access + /// their payload. + /// + /// Note: Uses its actual opcode value representation to easily convert + /// to and from its binary representation. + pub const Tag = enum(u8) { + /// Uses `nop` + @"unreachable" = 0x00, + /// Creates a new block that can be jump from. + /// + /// Type of the block is given in data `block_type` + block = 0x02, + /// Creates a new loop. + /// + /// Type of the loop is given in data `block_type` + loop = 0x03, + /// Represents the end of a function body or an initialization expression + /// + /// Payload is `nop` + end = 0x0B, + /// Breaks from the current block to a label + /// + /// Data is `label` where index represents the label to jump to + br = 0x0C, + /// Breaks from the current block if the stack value is non-zero + /// + /// Data is `label` where index represents the label to jump to + br_if = 0x0D, + /// Jump table that takes the stack value as an index where each value + /// represents the label to jump to. + /// + /// Data is extra of which the Payload's type is `JumpTable` + br_table = 0x0E, + /// Returns from the function + /// + /// Uses `nop` + @"return" = 0x0F, + /// Calls a function by its index + /// + /// Uses `label` + call = 0x10, + /// Loads a local at given index onto the stack. + /// + /// Uses `label` + local_get = 0x20, + /// Pops a value from the stack into the local at given index. + /// Stack value must be of the same type as the local. + /// + /// Uses `label` + local_set = 0x21, + /// Sets a local at given index using the value at the top of the stack without popping the value. + /// Stack value must have the same type as the local. + /// + /// Uses `label` + local_tee = 0x22, + /// Loads a (mutable) global at given index onto the stack + /// + /// Uses `label` + global_get = 0x23, + /// Pops a value from the stack and sets the global at given index. + /// Note: Both types must be equal and global must be marked mutable. + /// + /// Uses `label`. + global_set = 0x24, + /// Loads a 32-bit integer from memory (data section) onto the stack + /// Pops the value from the stack which represents the offset into memory. + /// + /// Uses `payload` of type `MemArg`. + i32_load = 0x28, + /// Pops 2 values from the stack, where the first value represents the value to write into memory + /// and the second value represents the offset into memory where the value must be written to. + /// + /// Uses `payload` of type `MemArg`. + i32_store = 0x36, + /// Returns the memory size in amount of pages. + /// + /// Uses `nop` + memory_size = 0x3F, + /// Increases the memory at by given number of pages. + /// + /// Uses `label` + memory_grow = 0x40, + /// Loads a 32-bit signed immediate value onto the stack + /// + /// Uses `imm32` + i32_const = 0x41, + /// Loads a i64-bit signed immediate value onto the stack + /// + /// uses `payload` of type `Imm64` + i64_const = 0x42, + /// Loads a 32-bit float value onto the stack. + /// + /// Uses `float32` + f32_const = 0x43, + /// Loads a 64-bit float value onto the stack. + /// + /// Uses `payload` of type `Float64` + f64_const = 0x44, + /// Uses `tag` + i32_eqz = 0x45, + /// Uses `tag` + i32_eq = 0x46, + /// Uses `tag` + i32_ne = 0x47, + /// Uses `tag` + i32_lt_s = 0x48, + /// Uses `tag` + i32_lt_u = 0x49, + /// Uses `tag` + i32_gt_s = 0x4A, + /// Uses `tag` + i32_gt_u = 0x4B, + /// Uses `tag` + i32_le_s = 0x4C, + /// Uses `tag` + i32_le_u = 0x4D, + /// Uses `tag` + i32_ge_s = 0x4E, + /// Uses `tag` + i32_ge_u = 0x4F, + /// Uses `tag` + i64_eqz = 0x50, + /// Uses `tag` + i64_eq = 0x51, + /// Uses `tag` + i64_ne = 0x52, + /// Uses `tag` + i64_lt_s = 0x53, + /// Uses `tag` + i64_lt_u = 0x54, + /// Uses `tag` + i64_gt_s = 0x55, + /// Uses `tag` + i64_gt_u = 0x56, + /// Uses `tag` + i64_le_s = 0x57, + /// Uses `tag` + i64_le_u = 0x58, + /// Uses `tag` + i64_ge_s = 0x59, + /// Uses `tag` + i64_ge_u = 0x5A, + /// Uses `tag` + f32_eq = 0x5B, + /// Uses `tag` + f32_ne = 0x5C, + /// Uses `tag` + f32_lt = 0x5D, + /// Uses `tag` + f32_gt = 0x5E, + /// Uses `tag` + f32_le = 0x5F, + /// Uses `tag` + f32_ge = 0x60, + /// Uses `tag` + f64_eq = 0x61, + /// Uses `tag` + f64_ne = 0x62, + /// Uses `tag` + f64_lt = 0x63, + /// Uses `tag` + f64_gt = 0x64, + /// Uses `tag` + f64_le = 0x65, + /// Uses `tag` + f64_ge = 0x66, + /// Uses `tag` + i32_add = 0x6A, + /// Uses `tag` + i32_sub = 0x6B, + /// Uses `tag` + i32_mul = 0x6C, + /// Uses `tag` + i32_div_s = 0x6D, + /// Uses `tag` + i32_div_u = 0x6E, + /// Uses `tag` + i32_and = 0x71, + /// Uses `tag` + i32_or = 0x72, + /// Uses `tag` + i32_xor = 0x73, + /// Uses `tag` + i32_shl = 0x74, + /// Uses `tag` + i32_shr_s = 0x75, + /// Uses `tag` + i32_shr_u = 0x76, + /// Uses `tag` + i64_add = 0x7C, + /// Uses `tag` + i64_sub = 0x7D, + /// Uses `tag` + i64_mul = 0x7E, + /// Uses `tag` + i64_div_s = 0x7F, + /// Uses `tag` + i64_div_u = 0x80, + /// Uses `tag` + i64_and = 0x83, + /// Uses `tag` + i32_wrap_i64 = 0xA7, + /// Uses `tag` + i64_extend_i32_s = 0xAC, + /// Uses `tag` + i64_extend_i32_u = 0xAD, + /// Uses `tag` + i32_extend8_s = 0xC0, + /// Uses `tag` + i32_extend16_s = 0xC1, + /// Uses `tag` + i64_extend8_s = 0xC2, + /// Uses `tag` + i64_extend16_s = 0xC3, + /// Uses `tag` + i64_extend32_s = 0xC4, + + /// From a given wasm opcode, returns a MIR tag. + pub fn fromOpcode(opcode: std.wasm.Opcode) Tag { + return @intToEnum(Tag, @enumToInt(opcode)); + } + + /// Returns a wasm opcode from a given MIR tag. + pub fn toOpcode(self: Tag) std.wasm.Opcode { + return @intToEnum(std.wasm.Opcode, @enumToInt(self)); + } + }; + + /// All instructions contain a 4-byte payload, which is contained within + /// this union. `Tag` determines which union tag is active, as well as + /// how to interpret the data within. + pub const Data = union { + /// Uses no additional data + tag: void, + /// Contains the result type of a block + /// + /// Used by `block` and `loop` + block_type: u8, + /// Contains an u32 index into a wasm section entry, such as a local. + /// Note: This is not an index to another instruction. + /// + /// Used by e.g. `local_get`, `local_set`, etc. + label: u32, + /// A 32-bit immediate value. + /// + /// Used by `i32_const` + imm32: i32, + /// A 32-bit float value + /// + /// Used by `f32_float` + float32: f32, + /// Index into `extra`. Meaning of what can be found there is context-dependent. + /// + /// Used by e.g. `br_table` + payload: u32, + }; +}; + +pub fn deinit(self: *Mir, gpa: *std.mem.Allocator) void { + self.instructions.deinit(gpa); + gpa.free(self.extra); + self.* = undefined; +} + +pub fn extraData(self: Mir, comptime T: type, index: usize) struct { data: T, end: usize } { + const fields = std.meta.fields(T); + var i: usize = index; + var result: T = undefined; + inline for (fields) |field| { + @field(result, field.name) = switch (field.field_type) { + u32 => self.extra[i], + else => |field_type| @compileError("Unsupported field type " ++ @typeName(field_type)), + }; + i += 1; + } + + return .{ .data = result, .end = i }; +} + +pub const JumpTable = struct { + /// Length of the jump table and the amount of entries it contains (includes default) + length: u32, +}; + +/// Stores an unsigned 64bit integer +/// into a 32bit most significant bits field +/// and a 32bit least significant bits field. +/// +/// This uses an unsigned integer rather than a signed integer +/// as we can easily store those into `extra` +pub const Imm64 = struct { + msb: u32, + lsb: u32, + + pub fn fromU64(imm: u64) Imm64 { + return .{ + .msb = @truncate(u32, imm >> 32), + .lsb = @truncate(u32, imm), + }; + } + + pub fn toU64(self: Imm64) u64 { + var result: u64 = 0; + result |= @as(u64, self.msb) << 32; + result |= @as(u64, self.lsb); + return result; + } +}; + +pub const Float64 = struct { + msb: u32, + lsb: u32, + + pub fn fromFloat64(float: f64) Float64 { + const tmp = @bitCast(u64, float); + return .{ + .msb = @truncate(u32, tmp >> 32), + .lsb = @truncate(u32, tmp), + }; + } + + pub fn toF64(self: Float64) f64 { + @bitCast(f64, self.toU64()); + } + + pub fn toU64(self: Float64) u64 { + var result: u64 = 0; + result |= @as(u64, self.msb) << 32; + result |= @as(u64, self.lsb); + return result; + } +}; + +pub const MemArg = struct { + offset: u32, + alignment: u32, +}; diff --git a/src/codegen/wasm.zig b/src/codegen/wasm.zig deleted file mode 100644 index 853fc75570..0000000000 --- a/src/codegen/wasm.zig +++ /dev/null @@ -1,1717 +0,0 @@ -const std = @import("std"); -const Allocator = std.mem.Allocator; -const ArrayList = std.ArrayList; -const assert = std.debug.assert; -const testing = std.testing; -const leb = std.leb; -const mem = std.mem; -const wasm = std.wasm; - -const Module = @import("../Module.zig"); -const Decl = Module.Decl; -const Type = @import("../type.zig").Type; -const Value = @import("../value.zig").Value; -const Compilation = @import("../Compilation.zig"); -const LazySrcLoc = Module.LazySrcLoc; -const link = @import("../link.zig"); -const TypedValue = @import("../TypedValue.zig"); -const Air = @import("../Air.zig"); -const Liveness = @import("../Liveness.zig"); - -/// Wasm Value, created when generating an instruction -const WValue = union(enum) { - /// May be referenced but is unused - none: void, - /// Index of the local variable - local: u32, - /// Holds a memoized typed value - constant: TypedValue, - /// Offset position in the list of bytecode instructions - code_offset: usize, - /// Used for variables that create multiple locals on the stack when allocated - /// such as structs and optionals. - multi_value: struct { - /// The index of the first local variable - index: u32, - /// The count of local variables this `WValue` consists of. - /// i.e. an ErrorUnion has a 'count' of 2. - count: u32, - }, -}; - -/// Wasm ops, but without input/output/signedness information -/// Used for `buildOpcode` -const Op = enum { - @"unreachable", - nop, - block, - loop, - @"if", - @"else", - end, - br, - br_if, - br_table, - @"return", - call, - call_indirect, - drop, - select, - local_get, - local_set, - local_tee, - global_get, - global_set, - load, - store, - memory_size, - memory_grow, - @"const", - eqz, - eq, - ne, - lt, - gt, - le, - ge, - clz, - ctz, - popcnt, - add, - sub, - mul, - div, - rem, - @"and", - @"or", - xor, - shl, - shr, - rotl, - rotr, - abs, - neg, - ceil, - floor, - trunc, - nearest, - sqrt, - min, - max, - copysign, - wrap, - convert, - demote, - promote, - reinterpret, - extend, -}; - -/// Contains the settings needed to create an `Opcode` using `buildOpcode`. -/// -/// The fields correspond to the opcode name. Here is an example -/// i32_trunc_f32_s -/// ^ ^ ^ ^ -/// | | | | -/// valtype1 | | | -/// = .i32 | | | -/// | | | -/// op | | -/// = .trunc | | -/// | | -/// valtype2 | -/// = .f32 | -/// | -/// width | -/// = null | -/// | -/// signed -/// = true -/// -/// There can be missing fields, here are some more examples: -/// i64_load8_u -/// --> .{ .valtype1 = .i64, .op = .load, .width = 8, signed = false } -/// i32_mul -/// --> .{ .valtype1 = .i32, .op = .trunc } -/// nop -/// --> .{ .op = .nop } -const OpcodeBuildArguments = struct { - /// First valtype in the opcode (usually represents the type of the output) - valtype1: ?wasm.Valtype = null, - /// The operation (e.g. call, unreachable, div, min, sqrt, etc.) - op: Op, - /// Width of the operation (e.g. 8 for i32_load8_s, 16 for i64_extend16_i32_s) - width: ?u8 = null, - /// Second valtype in the opcode name (usually represents the type of the input) - valtype2: ?wasm.Valtype = null, - /// Signedness of the op - signedness: ?std.builtin.Signedness = null, -}; - -/// Helper function that builds an Opcode given the arguments needed -fn buildOpcode(args: OpcodeBuildArguments) wasm.Opcode { - switch (args.op) { - .@"unreachable" => return .@"unreachable", - .nop => return .nop, - .block => return .block, - .loop => return .loop, - .@"if" => return .@"if", - .@"else" => return .@"else", - .end => return .end, - .br => return .br, - .br_if => return .br_if, - .br_table => return .br_table, - .@"return" => return .@"return", - .call => return .call, - .call_indirect => return .call_indirect, - .drop => return .drop, - .select => return .select, - .local_get => return .local_get, - .local_set => return .local_set, - .local_tee => return .local_tee, - .global_get => return .global_get, - .global_set => return .global_set, - - .load => if (args.width) |width| switch (width) { - 8 => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_load8_s else return .i32_load8_u, - .i64 => if (args.signedness.? == .signed) return .i64_load8_s else return .i64_load8_u, - .f32, .f64 => unreachable, - }, - 16 => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_load16_s else return .i32_load16_u, - .i64 => if (args.signedness.? == .signed) return .i64_load16_s else return .i64_load16_u, - .f32, .f64 => unreachable, - }, - 32 => switch (args.valtype1.?) { - .i64 => if (args.signedness.? == .signed) return .i64_load32_s else return .i64_load32_u, - .i32, .f32, .f64 => unreachable, - }, - else => unreachable, - } else switch (args.valtype1.?) { - .i32 => return .i32_load, - .i64 => return .i64_load, - .f32 => return .f32_load, - .f64 => return .f64_load, - }, - .store => if (args.width) |width| { - switch (width) { - 8 => switch (args.valtype1.?) { - .i32 => return .i32_store8, - .i64 => return .i64_store8, - .f32, .f64 => unreachable, - }, - 16 => switch (args.valtype1.?) { - .i32 => return .i32_store16, - .i64 => return .i64_store16, - .f32, .f64 => unreachable, - }, - 32 => switch (args.valtype1.?) { - .i64 => return .i64_store32, - .i32, .f32, .f64 => unreachable, - }, - else => unreachable, - } - } else { - switch (args.valtype1.?) { - .i32 => return .i32_store, - .i64 => return .i64_store, - .f32 => return .f32_store, - .f64 => return .f64_store, - } - }, - - .memory_size => return .memory_size, - .memory_grow => return .memory_grow, - - .@"const" => switch (args.valtype1.?) { - .i32 => return .i32_const, - .i64 => return .i64_const, - .f32 => return .f32_const, - .f64 => return .f64_const, - }, - - .eqz => switch (args.valtype1.?) { - .i32 => return .i32_eqz, - .i64 => return .i64_eqz, - .f32, .f64 => unreachable, - }, - .eq => switch (args.valtype1.?) { - .i32 => return .i32_eq, - .i64 => return .i64_eq, - .f32 => return .f32_eq, - .f64 => return .f64_eq, - }, - .ne => switch (args.valtype1.?) { - .i32 => return .i32_ne, - .i64 => return .i64_ne, - .f32 => return .f32_ne, - .f64 => return .f64_ne, - }, - - .lt => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_lt_s else return .i32_lt_u, - .i64 => if (args.signedness.? == .signed) return .i64_lt_s else return .i64_lt_u, - .f32 => return .f32_lt, - .f64 => return .f64_lt, - }, - .gt => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_gt_s else return .i32_gt_u, - .i64 => if (args.signedness.? == .signed) return .i64_gt_s else return .i64_gt_u, - .f32 => return .f32_gt, - .f64 => return .f64_gt, - }, - .le => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_le_s else return .i32_le_u, - .i64 => if (args.signedness.? == .signed) return .i64_le_s else return .i64_le_u, - .f32 => return .f32_le, - .f64 => return .f64_le, - }, - .ge => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_ge_s else return .i32_ge_u, - .i64 => if (args.signedness.? == .signed) return .i64_ge_s else return .i64_ge_u, - .f32 => return .f32_ge, - .f64 => return .f64_ge, - }, - - .clz => switch (args.valtype1.?) { - .i32 => return .i32_clz, - .i64 => return .i64_clz, - .f32, .f64 => unreachable, - }, - .ctz => switch (args.valtype1.?) { - .i32 => return .i32_ctz, - .i64 => return .i64_ctz, - .f32, .f64 => unreachable, - }, - .popcnt => switch (args.valtype1.?) { - .i32 => return .i32_popcnt, - .i64 => return .i64_popcnt, - .f32, .f64 => unreachable, - }, - - .add => switch (args.valtype1.?) { - .i32 => return .i32_add, - .i64 => return .i64_add, - .f32 => return .f32_add, - .f64 => return .f64_add, - }, - .sub => switch (args.valtype1.?) { - .i32 => return .i32_sub, - .i64 => return .i64_sub, - .f32 => return .f32_sub, - .f64 => return .f64_sub, - }, - .mul => switch (args.valtype1.?) { - .i32 => return .i32_mul, - .i64 => return .i64_mul, - .f32 => return .f32_mul, - .f64 => return .f64_mul, - }, - - .div => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_div_s else return .i32_div_u, - .i64 => if (args.signedness.? == .signed) return .i64_div_s else return .i64_div_u, - .f32 => return .f32_div, - .f64 => return .f64_div, - }, - .rem => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_rem_s else return .i32_rem_u, - .i64 => if (args.signedness.? == .signed) return .i64_rem_s else return .i64_rem_u, - .f32, .f64 => unreachable, - }, - - .@"and" => switch (args.valtype1.?) { - .i32 => return .i32_and, - .i64 => return .i64_and, - .f32, .f64 => unreachable, - }, - .@"or" => switch (args.valtype1.?) { - .i32 => return .i32_or, - .i64 => return .i64_or, - .f32, .f64 => unreachable, - }, - .xor => switch (args.valtype1.?) { - .i32 => return .i32_xor, - .i64 => return .i64_xor, - .f32, .f64 => unreachable, - }, - - .shl => switch (args.valtype1.?) { - .i32 => return .i32_shl, - .i64 => return .i64_shl, - .f32, .f64 => unreachable, - }, - .shr => switch (args.valtype1.?) { - .i32 => if (args.signedness.? == .signed) return .i32_shr_s else return .i32_shr_u, - .i64 => if (args.signedness.? == .signed) return .i64_shr_s else return .i64_shr_u, - .f32, .f64 => unreachable, - }, - .rotl => switch (args.valtype1.?) { - .i32 => return .i32_rotl, - .i64 => return .i64_rotl, - .f32, .f64 => unreachable, - }, - .rotr => switch (args.valtype1.?) { - .i32 => return .i32_rotr, - .i64 => return .i64_rotr, - .f32, .f64 => unreachable, - }, - - .abs => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_abs, - .f64 => return .f64_abs, - }, - .neg => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_neg, - .f64 => return .f64_neg, - }, - .ceil => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_ceil, - .f64 => return .f64_ceil, - }, - .floor => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_floor, - .f64 => return .f64_floor, - }, - .trunc => switch (args.valtype1.?) { - .i32 => switch (args.valtype2.?) { - .i32 => unreachable, - .i64 => unreachable, - .f32 => if (args.signedness.? == .signed) return .i32_trunc_f32_s else return .i32_trunc_f32_u, - .f64 => if (args.signedness.? == .signed) return .i32_trunc_f64_s else return .i32_trunc_f64_u, - }, - .i64 => unreachable, - .f32 => return .f32_trunc, - .f64 => return .f64_trunc, - }, - .nearest => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_nearest, - .f64 => return .f64_nearest, - }, - .sqrt => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_sqrt, - .f64 => return .f64_sqrt, - }, - .min => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_min, - .f64 => return .f64_min, - }, - .max => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_max, - .f64 => return .f64_max, - }, - .copysign => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => return .f32_copysign, - .f64 => return .f64_copysign, - }, - - .wrap => switch (args.valtype1.?) { - .i32 => switch (args.valtype2.?) { - .i32 => unreachable, - .i64 => return .i32_wrap_i64, - .f32, .f64 => unreachable, - }, - .i64, .f32, .f64 => unreachable, - }, - .convert => switch (args.valtype1.?) { - .i32, .i64 => unreachable, - .f32 => switch (args.valtype2.?) { - .i32 => if (args.signedness.? == .signed) return .f32_convert_i32_s else return .f32_convert_i32_u, - .i64 => if (args.signedness.? == .signed) return .f32_convert_i64_s else return .f32_convert_i64_u, - .f32, .f64 => unreachable, - }, - .f64 => switch (args.valtype2.?) { - .i32 => if (args.signedness.? == .signed) return .f64_convert_i32_s else return .f64_convert_i32_u, - .i64 => if (args.signedness.? == .signed) return .f64_convert_i64_s else return .f64_convert_i64_u, - .f32, .f64 => unreachable, - }, - }, - .demote => if (args.valtype1.? == .f32 and args.valtype2.? == .f64) return .f32_demote_f64 else unreachable, - .promote => if (args.valtype1.? == .f64 and args.valtype2.? == .f32) return .f64_promote_f32 else unreachable, - .reinterpret => switch (args.valtype1.?) { - .i32 => if (args.valtype2.? == .f32) return .i32_reinterpret_f32 else unreachable, - .i64 => if (args.valtype2.? == .f64) return .i64_reinterpret_f64 else unreachable, - .f32 => if (args.valtype2.? == .i32) return .f32_reinterpret_i32 else unreachable, - .f64 => if (args.valtype2.? == .i64) return .f64_reinterpret_i64 else unreachable, - }, - .extend => switch (args.valtype1.?) { - .i32 => switch (args.width.?) { - 8 => if (args.signedness.? == .signed) return .i32_extend8_s else unreachable, - 16 => if (args.signedness.? == .signed) return .i32_extend16_s else unreachable, - else => unreachable, - }, - .i64 => switch (args.width.?) { - 8 => if (args.signedness.? == .signed) return .i64_extend8_s else unreachable, - 16 => if (args.signedness.? == .signed) return .i64_extend16_s else unreachable, - 32 => if (args.signedness.? == .signed) return .i64_extend32_s else unreachable, - else => unreachable, - }, - .f32, .f64 => unreachable, - }, - } -} - -test "Wasm - buildOpcode" { - // Make sure buildOpcode is referenced, and test some examples - const i32_const = buildOpcode(.{ .op = .@"const", .valtype1 = .i32 }); - const end = buildOpcode(.{ .op = .end }); - const local_get = buildOpcode(.{ .op = .local_get }); - const i64_extend32_s = buildOpcode(.{ .op = .extend, .valtype1 = .i64, .width = 32, .signedness = .signed }); - const f64_reinterpret_i64 = buildOpcode(.{ .op = .reinterpret, .valtype1 = .f64, .valtype2 = .i64 }); - - try testing.expectEqual(@as(wasm.Opcode, .i32_const), i32_const); - try testing.expectEqual(@as(wasm.Opcode, .end), end); - try testing.expectEqual(@as(wasm.Opcode, .local_get), local_get); - try testing.expectEqual(@as(wasm.Opcode, .i64_extend32_s), i64_extend32_s); - try testing.expectEqual(@as(wasm.Opcode, .f64_reinterpret_i64), f64_reinterpret_i64); -} - -pub const Result = union(enum) { - /// The codegen bytes have been appended to `Context.code` - appended: void, - /// The data is managed externally and are part of the `Result` - externally_managed: []const u8, -}; - -/// Hashmap to store generated `WValue` for each `Air.Inst.Ref` -pub const ValueTable = std.AutoHashMapUnmanaged(Air.Inst.Index, WValue); - -/// Code represents the `Code` section of wasm that -/// belongs to a function -pub const Context = struct { - /// Reference to the function declaration the code - /// section belongs to - decl: *Decl, - air: Air, - liveness: Liveness, - gpa: *mem.Allocator, - /// Table to save `WValue`'s generated by an `Air.Inst` - values: ValueTable, - /// Mapping from Air.Inst.Index to block ids - blocks: std.AutoArrayHashMapUnmanaged(Air.Inst.Index, u32) = .{}, - /// `bytes` contains the wasm bytecode belonging to the 'code' section. - code: ArrayList(u8), - /// Contains the generated function type bytecode for the current function - /// found in `decl` - func_type_data: ArrayList(u8), - /// The index the next local generated will have - /// NOTE: arguments share the index with locals therefore the first variable - /// will have the index that comes after the last argument's index - local_index: u32 = 0, - /// If codegen fails, an error messages will be allocated and saved in `err_msg` - err_msg: *Module.ErrorMsg, - /// Current block depth. Used to calculate the relative difference between a break - /// and block - block_depth: u32 = 0, - /// List of all locals' types generated throughout this declaration - /// used to emit locals count at start of 'code' section. - locals: std.ArrayListUnmanaged(u8), - /// The Target we're emitting (used to call intInfo) - target: std.Target, - /// Table with the global error set. Consists of every error found in - /// the compiled code. Each error name maps to a `Module.ErrorInt` which is emitted - /// during codegen to determine the error value. - global_error_set: std.StringHashMapUnmanaged(Module.ErrorInt), - - const InnerError = error{ - OutOfMemory, - CodegenFail, - /// Can occur when dereferencing a pointer that points to a `Decl` of which the analysis has failed - AnalysisFail, - }; - - pub fn deinit(self: *Context) void { - self.values.deinit(self.gpa); - self.blocks.deinit(self.gpa); - self.locals.deinit(self.gpa); - self.* = undefined; - } - - /// Sets `err_msg` on `Context` and returns `error.CodegemFail` which is caught in link/Wasm.zig - fn fail(self: *Context, comptime fmt: []const u8, args: anytype) InnerError { - const src: LazySrcLoc = .{ .node_offset = 0 }; - const src_loc = src.toSrcLoc(self.decl); - self.err_msg = try Module.ErrorMsg.create(self.gpa, src_loc, fmt, args); - return error.CodegenFail; - } - - /// Resolves the `WValue` for the given instruction `inst` - /// When the given instruction has a `Value`, it returns a constant instead - fn resolveInst(self: Context, ref: Air.Inst.Ref) WValue { - const inst_index = Air.refToIndex(ref) orelse { - const tv = Air.Inst.Ref.typed_value_map[@enumToInt(ref)]; - if (!tv.ty.hasCodeGenBits()) { - return WValue.none; - } - return WValue{ .constant = tv }; - }; - - const inst_type = self.air.typeOfIndex(inst_index); - if (!inst_type.hasCodeGenBits()) return .none; - - if (self.air.instructions.items(.tag)[inst_index] == .constant) { - const ty_pl = self.air.instructions.items(.data)[inst_index].ty_pl; - return WValue{ .constant = .{ .ty = inst_type, .val = self.air.values[ty_pl.payload] } }; - } - - return self.values.get(inst_index).?; // Instruction does not dominate all uses! - } - - /// Using a given `Type`, returns the corresponding wasm Valtype - fn typeToValtype(self: *Context, ty: Type) InnerError!wasm.Valtype { - return switch (ty.zigTypeTag()) { - .Float => blk: { - const bits = ty.floatBits(self.target); - if (bits == 16 or bits == 32) break :blk wasm.Valtype.f32; - if (bits == 64) break :blk wasm.Valtype.f64; - return self.fail("Float bit size not supported by wasm: '{d}'", .{bits}); - }, - .Int => blk: { - const info = ty.intInfo(self.target); - if (info.bits <= 32) break :blk wasm.Valtype.i32; - if (info.bits > 32 and info.bits <= 64) break :blk wasm.Valtype.i64; - return self.fail("Integer bit size not supported by wasm: '{d}'", .{info.bits}); - }, - .Enum => switch (ty.tag()) { - .enum_simple => wasm.Valtype.i32, - else => self.typeToValtype(ty.cast(Type.Payload.EnumFull).?.data.tag_ty), - }, - .Bool, - .Pointer, - .ErrorSet, - => wasm.Valtype.i32, - .Struct, .ErrorUnion, .Optional => unreachable, // Multi typed, must be handled individually. - else => |tag| self.fail("TODO - Wasm valtype for type '{s}'", .{tag}), - }; - } - - /// Using a given `Type`, returns the byte representation of its wasm value type - fn genValtype(self: *Context, ty: Type) InnerError!u8 { - return wasm.valtype(try self.typeToValtype(ty)); - } - - /// Using a given `Type`, returns the corresponding wasm value type - /// Differently from `genValtype` this also allows `void` to create a block - /// with no return type - fn genBlockType(self: *Context, ty: Type) InnerError!u8 { - return switch (ty.tag()) { - .void, .noreturn => wasm.block_empty, - else => self.genValtype(ty), - }; - } - - /// Writes the bytecode depending on the given `WValue` in `val` - fn emitWValue(self: *Context, val: WValue) InnerError!void { - const writer = self.code.writer(); - switch (val) { - .multi_value => unreachable, // multi_value can never be written directly, and must be accessed individually - .none, .code_offset => {}, // no-op - .local => |idx| { - try writer.writeByte(wasm.opcode(.local_get)); - try leb.writeULEB128(writer, idx); - }, - .constant => |tv| try self.emitConstant(tv.val, tv.ty), // Creates a new constant on the stack - } - } - - /// Creates one or multiple locals for a given `Type`. - /// Returns a corresponding `Wvalue` that can either be of tag - /// local or multi_value - fn allocLocal(self: *Context, ty: Type) InnerError!WValue { - const initial_index = self.local_index; - switch (ty.zigTypeTag()) { - .Struct => { - // for each struct field, generate a local - const struct_data: *Module.Struct = ty.castTag(.@"struct").?.data; - const fields_len = @intCast(u32, struct_data.fields.count()); - try self.locals.ensureUnusedCapacity(self.gpa, fields_len); - for (struct_data.fields.values()) |*value| { - const val_type = try self.genValtype(value.ty); - self.locals.appendAssumeCapacity(val_type); - self.local_index += 1; - } - return WValue{ .multi_value = .{ - .index = initial_index, - .count = fields_len, - } }; - }, - .ErrorUnion => { - const payload_type = ty.errorUnionPayload(); - const val_type = try self.genValtype(payload_type); - - // we emit the error value as the first local, and the payload as the following. - // The first local is also used to find the index of the error and payload. - // - // TODO: Add support where the payload is a type that contains multiple locals such as a struct. - try self.locals.ensureUnusedCapacity(self.gpa, 2); - self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // error values are always i32 - self.locals.appendAssumeCapacity(val_type); - self.local_index += 2; - - return WValue{ .multi_value = .{ - .index = initial_index, - .count = 2, - } }; - }, - .Optional => { - var opt_buf: Type.Payload.ElemType = undefined; - const child_type = ty.optionalChild(&opt_buf); - if (ty.isPtrLikeOptional()) { - return self.fail("TODO: wasm optional pointer", .{}); - } - - try self.locals.ensureUnusedCapacity(self.gpa, 2); - self.locals.appendAssumeCapacity(wasm.valtype(.i32)); // optional 'tag' for null-checking is always i32 - self.locals.appendAssumeCapacity(try self.genValtype(child_type)); - self.local_index += 2; - - return WValue{ .multi_value = .{ - .index = initial_index, - .count = 2, - } }; - }, - else => { - const valtype = try self.genValtype(ty); - try self.locals.append(self.gpa, valtype); - self.local_index += 1; - return WValue{ .local = initial_index }; - }, - } - } - - fn genFunctype(self: *Context) InnerError!void { - assert(self.decl.has_tv); - const ty = self.decl.ty; - const writer = self.func_type_data.writer(); - - try writer.writeByte(wasm.function_type); - - // param types - try leb.writeULEB128(writer, @intCast(u32, ty.fnParamLen())); - if (ty.fnParamLen() != 0) { - const params = try self.gpa.alloc(Type, ty.fnParamLen()); - defer self.gpa.free(params); - ty.fnParamTypes(params); - for (params) |param_type| { - // Can we maybe get the source index of each param? - const val_type = try self.genValtype(param_type); - try writer.writeByte(val_type); - } - } - - // return type - const return_type = ty.fnReturnType(); - switch (return_type.zigTypeTag()) { - .Void, .NoReturn => try leb.writeULEB128(writer, @as(u32, 0)), - .Struct => return self.fail("TODO: Implement struct as return type for wasm", .{}), - .Optional => return self.fail("TODO: Implement optionals as return type for wasm", .{}), - .ErrorUnion => { - const val_type = try self.genValtype(return_type.errorUnionPayload()); - - // write down the amount of return values - try leb.writeULEB128(writer, @as(u32, 2)); - try writer.writeByte(wasm.valtype(.i32)); // error code is always an i32 integer. - try writer.writeByte(val_type); - }, - else => { - try leb.writeULEB128(writer, @as(u32, 1)); - // Can we maybe get the source index of the return type? - const val_type = try self.genValtype(return_type); - try writer.writeByte(val_type); - }, - } - } - - pub fn genFunc(self: *Context) InnerError!Result { - try self.genFunctype(); - // TODO: check for and handle death of instructions - - // Reserve space to write the size after generating the code as well as space for locals count - try self.code.resize(10); - - try self.genBody(self.air.getMainBody()); - - // finally, write our local types at the 'offset' position - { - leb.writeUnsignedFixed(5, self.code.items[5..10], @intCast(u32, self.locals.items.len)); - - // offset into 'code' section where we will put our locals types - var local_offset: usize = 10; - - // emit the actual locals amount - for (self.locals.items) |local| { - var buf: [6]u8 = undefined; - leb.writeUnsignedFixed(5, buf[0..5], @as(u32, 1)); - buf[5] = local; - try self.code.insertSlice(local_offset, &buf); - local_offset += 6; - } - } - - const writer = self.code.writer(); - try writer.writeByte(wasm.opcode(.end)); - - // Fill in the size of the generated code to the reserved space at the - // beginning of the buffer. - const size = self.code.items.len - 5 + self.decl.fn_link.wasm.idx_refs.items.len * 5; - leb.writeUnsignedFixed(5, self.code.items[0..5], @intCast(u32, size)); - - // codegen data has been appended to `code` - return Result.appended; - } - - /// Generates the wasm bytecode for the declaration belonging to `Context` - pub fn gen(self: *Context, ty: Type, val: Value) InnerError!Result { - switch (ty.zigTypeTag()) { - .Fn => { - try self.genFunctype(); - if (val.tag() == .extern_fn) { - return Result.appended; // don't need code body for extern functions - } - return self.fail("TODO implement wasm codegen for function pointers", .{}); - }, - .Array => { - if (val.castTag(.bytes)) |payload| { - if (ty.sentinel()) |sentinel| { - try self.code.appendSlice(payload.data); - - switch (try self.gen(ty.elemType(), sentinel)) { - .appended => return Result.appended, - .externally_managed => |data| { - try self.code.appendSlice(data); - return Result.appended; - }, - } - } - return Result{ .externally_managed = payload.data }; - } else return self.fail("TODO implement gen for more kinds of arrays", .{}); - }, - .Int => { - const info = ty.intInfo(self.target); - if (info.bits == 8 and info.signedness == .unsigned) { - const int_byte = val.toUnsignedInt(); - try self.code.append(@intCast(u8, int_byte)); - return Result.appended; - } - return self.fail("TODO: Implement codegen for int type: '{}'", .{ty}); - }, - .Enum => { - try self.emitConstant(val, ty); - return Result.appended; - }, - .Struct => { - // TODO write the fields for real - try self.code.writer().writeByteNTimes(0xaa, ty.abiSize(self.target)); - return Result{ .appended = {} }; - }, - else => |tag| return self.fail("TODO: Implement zig type codegen for type: '{s}'", .{tag}), - } - } - - fn genInst(self: *Context, inst: Air.Inst.Index) !WValue { - const air_tags = self.air.instructions.items(.tag); - return switch (air_tags[inst]) { - .add => self.airBinOp(inst, .add), - .addwrap => self.airWrapBinOp(inst, .add), - .sub => self.airBinOp(inst, .sub), - .subwrap => self.airWrapBinOp(inst, .sub), - .mul => self.airBinOp(inst, .mul), - .mulwrap => self.airWrapBinOp(inst, .mul), - .div_trunc => self.airBinOp(inst, .div), - .bit_and => self.airBinOp(inst, .@"and"), - .bit_or => self.airBinOp(inst, .@"or"), - .bool_and => self.airBinOp(inst, .@"and"), - .bool_or => self.airBinOp(inst, .@"or"), - .xor => self.airBinOp(inst, .xor), - - .cmp_eq => self.airCmp(inst, .eq), - .cmp_gte => self.airCmp(inst, .gte), - .cmp_gt => self.airCmp(inst, .gt), - .cmp_lte => self.airCmp(inst, .lte), - .cmp_lt => self.airCmp(inst, .lt), - .cmp_neq => self.airCmp(inst, .neq), - - .alloc => self.airAlloc(inst), - .arg => self.airArg(inst), - .bitcast => self.airBitcast(inst), - .block => self.airBlock(inst), - .breakpoint => self.airBreakpoint(inst), - .br => self.airBr(inst), - .call => self.airCall(inst), - .cond_br => self.airCondBr(inst), - .constant => unreachable, - .dbg_stmt => WValue.none, - .intcast => self.airIntcast(inst), - - .is_err => self.airIsErr(inst, .i32_ne), - .is_non_err => self.airIsErr(inst, .i32_eq), - - .is_null => self.airIsNull(inst, .i32_ne), - .is_non_null => self.airIsNull(inst, .i32_eq), - .is_null_ptr => self.airIsNull(inst, .i32_ne), - .is_non_null_ptr => self.airIsNull(inst, .i32_eq), - - .load => self.airLoad(inst), - .loop => self.airLoop(inst), - .not => self.airNot(inst), - .ret => self.airRet(inst), - .store => self.airStore(inst), - .struct_field_ptr => self.airStructFieldPtr(inst), - .struct_field_ptr_index_0 => self.airStructFieldPtrIndex(inst, 0), - .struct_field_ptr_index_1 => self.airStructFieldPtrIndex(inst, 1), - .struct_field_ptr_index_2 => self.airStructFieldPtrIndex(inst, 2), - .struct_field_ptr_index_3 => self.airStructFieldPtrIndex(inst, 3), - .struct_field_val => self.airStructFieldVal(inst), - .switch_br => self.airSwitchBr(inst), - .unreach => self.airUnreachable(inst), - .wrap_optional => self.airWrapOptional(inst), - - .unwrap_errunion_payload => self.airUnwrapErrUnionPayload(inst), - .wrap_errunion_payload => self.airWrapErrUnionPayload(inst), - - .optional_payload => self.airOptionalPayload(inst), - .optional_payload_ptr => self.airOptionalPayload(inst), - .optional_payload_ptr_set => self.airOptionalPayloadPtrSet(inst), - else => |tag| self.fail("TODO: Implement wasm inst: {s}", .{@tagName(tag)}), - }; - } - - fn genBody(self: *Context, body: []const Air.Inst.Index) InnerError!void { - for (body) |inst| { - const result = try self.genInst(inst); - try self.values.putNoClobber(self.gpa, inst, result); - } - } - - fn airRet(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const un_op = self.air.instructions.items(.data)[inst].un_op; - const operand = self.resolveInst(un_op); - try self.emitWValue(operand); - try self.code.append(wasm.opcode(.@"return")); - return .none; - } - - fn airCall(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const pl_op = self.air.instructions.items(.data)[inst].pl_op; - const extra = self.air.extraData(Air.Call, pl_op.payload); - const args = self.air.extra[extra.end..][0..extra.data.args_len]; - - const target: *Decl = blk: { - const func_val = self.air.value(pl_op.operand).?; - - if (func_val.castTag(.function)) |func| { - break :blk func.data.owner_decl; - } else if (func_val.castTag(.extern_fn)) |ext_fn| { - break :blk ext_fn.data; - } - return self.fail("Expected a function, but instead found type '{s}'", .{func_val.tag()}); - }; - - for (args) |arg| { - const arg_val = self.resolveInst(@intToEnum(Air.Inst.Ref, arg)); - try self.emitWValue(arg_val); - } - - try self.code.append(wasm.opcode(.call)); - - // The function index immediate argument will be filled in using this data - // in link.Wasm.flush(). - try self.decl.fn_link.wasm.idx_refs.append(self.gpa, .{ - .offset = @intCast(u32, self.code.items.len), - .decl = target, - }); - - return .none; - } - - fn airAlloc(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const elem_type = self.air.typeOfIndex(inst).elemType(); - return self.allocLocal(elem_type); - } - - fn airStore(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const writer = self.code.writer(); - - const lhs = self.resolveInst(bin_op.lhs); - const rhs = self.resolveInst(bin_op.rhs); - - switch (lhs) { - .multi_value => |multi_value| switch (rhs) { - // When assigning a value to a multi_value such as a struct, - // we simply assign the local_index to the rhs one. - // This allows us to update struct fields without having to individually - // set each local as each field's index will be calculated off the struct's base index - .multi_value => self.values.put(self.gpa, Air.refToIndex(bin_op.lhs).?, rhs) catch unreachable, // Instruction does not dominate all uses! - .constant, .none => { - // emit all values onto the stack if constant - try self.emitWValue(rhs); - - // for each local, pop the stack value into the local - // As the last element is on top of the stack, we must populate the locals - // in reverse. - var i: u32 = multi_value.count; - while (i > 0) : (i -= 1) { - try writer.writeByte(wasm.opcode(.local_set)); - try leb.writeULEB128(writer, multi_value.index + i - 1); - } - }, - .local => { - // This can occur when we wrap a single value into a multi-value, - // such as wrapping a non-optional value into an optional. - // This means we must zero the null-tag, and set the payload. - assert(multi_value.count == 2); - // set null-tag - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, @as(u32, 0)); - try writer.writeByte(wasm.opcode(.local_set)); - try leb.writeULEB128(writer, multi_value.index); - - // set payload - try self.emitWValue(rhs); - try writer.writeByte(wasm.opcode(.local_set)); - try leb.writeULEB128(writer, multi_value.index + 1); - }, - else => unreachable, - }, - .local => |local| { - try self.emitWValue(rhs); - try writer.writeByte(wasm.opcode(.local_set)); - try leb.writeULEB128(writer, local); - }, - else => unreachable, - } - return .none; - } - - fn airLoad(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - return self.resolveInst(ty_op.operand); - } - - fn airArg(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - _ = inst; - // arguments share the index with locals - defer self.local_index += 1; - return WValue{ .local = self.local_index }; - } - - fn airBinOp(self: *Context, inst: Air.Inst.Index, op: Op) InnerError!WValue { - const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const lhs = self.resolveInst(bin_op.lhs); - const rhs = self.resolveInst(bin_op.rhs); - - // it's possible for both lhs and/or rhs to return an offset as well, - // in which case we return the first offset occurrence we find. - const offset = blk: { - if (lhs == .code_offset) break :blk lhs.code_offset; - if (rhs == .code_offset) break :blk rhs.code_offset; - break :blk self.code.items.len; - }; - - try self.emitWValue(lhs); - try self.emitWValue(rhs); - - const bin_ty = self.air.typeOf(bin_op.lhs); - const opcode: wasm.Opcode = buildOpcode(.{ - .op = op, - .valtype1 = try self.typeToValtype(bin_ty), - .signedness = if (bin_ty.isSignedInt()) .signed else .unsigned, - }); - try self.code.append(wasm.opcode(opcode)); - return WValue{ .code_offset = offset }; - } - - fn airWrapBinOp(self: *Context, inst: Air.Inst.Index, op: Op) InnerError!WValue { - const bin_op = self.air.instructions.items(.data)[inst].bin_op; - const lhs = self.resolveInst(bin_op.lhs); - const rhs = self.resolveInst(bin_op.rhs); - - // it's possible for both lhs and/or rhs to return an offset as well, - // in which case we return the first offset occurrence we find. - const offset = blk: { - if (lhs == .code_offset) break :blk lhs.code_offset; - if (rhs == .code_offset) break :blk rhs.code_offset; - break :blk self.code.items.len; - }; - - try self.emitWValue(lhs); - try self.emitWValue(rhs); - - const bin_ty = self.air.typeOf(bin_op.lhs); - const opcode: wasm.Opcode = buildOpcode(.{ - .op = op, - .valtype1 = try self.typeToValtype(bin_ty), - .signedness = if (bin_ty.isSignedInt()) .signed else .unsigned, - }); - try self.code.append(wasm.opcode(opcode)); - - const int_info = bin_ty.intInfo(self.target); - const bitsize = int_info.bits; - const is_signed = int_info.signedness == .signed; - // if target type bitsize is x < 32 and 32 > x < 64, we perform - // result & ((1< 64) { - return self.fail("TODO wasm: Integer wrapping for bitsizes larger than 64", .{}); - } - - return WValue{ .code_offset = offset }; - } - - fn emitConstant(self: *Context, val: Value, ty: Type) InnerError!void { - const writer = self.code.writer(); - switch (ty.zigTypeTag()) { - .Int => { - // write opcode - const opcode: wasm.Opcode = buildOpcode(.{ - .op = .@"const", - .valtype1 = try self.typeToValtype(ty), - }); - try writer.writeByte(wasm.opcode(opcode)); - const int_info = ty.intInfo(self.target); - // write constant - switch (int_info.signedness) { - .signed => try leb.writeILEB128(writer, val.toSignedInt()), - .unsigned => switch (int_info.bits) { - 0...32 => try leb.writeILEB128(writer, @bitCast(i32, @intCast(u32, val.toUnsignedInt()))), - 33...64 => try leb.writeILEB128(writer, @bitCast(i64, val.toUnsignedInt())), - else => |bits| return self.fail("Wasm TODO: emitConstant for integer with {d} bits", .{bits}), - }, - } - }, - .Bool => { - // write opcode - try writer.writeByte(wasm.opcode(.i32_const)); - // write constant - try leb.writeILEB128(writer, val.toSignedInt()); - }, - .Float => { - // write opcode - const opcode: wasm.Opcode = buildOpcode(.{ - .op = .@"const", - .valtype1 = try self.typeToValtype(ty), - }); - try writer.writeByte(wasm.opcode(opcode)); - // write constant - switch (ty.floatBits(self.target)) { - 0...32 => try writer.writeIntLittle(u32, @bitCast(u32, val.toFloat(f32))), - 64 => try writer.writeIntLittle(u64, @bitCast(u64, val.toFloat(f64))), - else => |bits| return self.fail("Wasm TODO: emitConstant for float with {d} bits", .{bits}), - } - }, - .Pointer => { - if (val.castTag(.decl_ref)) |payload| { - const decl = payload.data; - decl.alive = true; - - // offset into the offset table within the 'data' section - const ptr_width = self.target.cpu.arch.ptrBitWidth() / 8; - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, decl.link.wasm.offset_index * ptr_width); - - // memory instruction followed by their memarg immediate - // memarg ::== x:u32, y:u32 => {align x, offset y} - try writer.writeByte(wasm.opcode(.i32_load)); - try leb.writeULEB128(writer, @as(u32, 0)); - try leb.writeULEB128(writer, @as(u32, 0)); - } else return self.fail("Wasm TODO: emitConstant for other const pointer tag {s}", .{val.tag()}); - }, - .Void => {}, - .Enum => { - if (val.castTag(.enum_field_index)) |field_index| { - switch (ty.tag()) { - .enum_simple => { - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, field_index.data); - }, - .enum_full, .enum_nonexhaustive => { - const enum_full = ty.cast(Type.Payload.EnumFull).?.data; - if (enum_full.values.count() != 0) { - const tag_val = enum_full.values.keys()[field_index.data]; - try self.emitConstant(tag_val, enum_full.tag_ty); - } else { - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, field_index.data); - } - }, - else => unreachable, - } - } else { - var int_tag_buffer: Type.Payload.Bits = undefined; - const int_tag_ty = ty.intTagType(&int_tag_buffer); - try self.emitConstant(val, int_tag_ty); - } - }, - .ErrorSet => { - const error_index = self.global_error_set.get(val.getError().?).?; - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, error_index); - }, - .ErrorUnion => { - const error_type = ty.errorUnionSet(); - const payload_type = ty.errorUnionPayload(); - if (val.castTag(.eu_payload)) |pl| { - const payload_val = pl.data; - // no error, so write a '0' const - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeULEB128(writer, @as(u32, 0)); - // after the error code, we emit the payload - try self.emitConstant(payload_val, payload_type); - } else { - // write the error val - try self.emitConstant(val, error_type); - - // no payload, so write a '0' const - const opcode: wasm.Opcode = buildOpcode(.{ - .op = .@"const", - .valtype1 = try self.typeToValtype(payload_type), - }); - try writer.writeByte(wasm.opcode(opcode)); - try leb.writeULEB128(writer, @as(u32, 0)); - } - }, - .Optional => { - var buf: Type.Payload.ElemType = undefined; - const payload_type = ty.optionalChild(&buf); - if (ty.isPtrLikeOptional()) { - return self.fail("Wasm TODO: emitConstant for optional pointer", .{}); - } - - // When constant has value 'null', set is_null local to '1' - // and payload to '0' - if (val.castTag(.opt_payload)) |pl| { - const payload_val = pl.data; - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeILEB128(writer, @as(i32, 0)); - try self.emitConstant(payload_val, payload_type); - } else { - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeILEB128(writer, @as(i32, 1)); - - const opcode: wasm.Opcode = buildOpcode(.{ - .op = .@"const", - .valtype1 = try self.typeToValtype(payload_type), - }); - try writer.writeByte(wasm.opcode(opcode)); - try leb.writeULEB128(writer, @as(u32, 0)); - } - }, - else => |zig_type| return self.fail("Wasm TODO: emitConstant for zigTypeTag {s}", .{zig_type}), - } - } - - /// Returns a `Value` as a signed 32 bit value. - /// It's illegal to provide a value with a type that cannot be represented - /// as an integer value. - fn valueAsI32(self: Context, val: Value, ty: Type) i32 { - switch (ty.zigTypeTag()) { - .Enum => { - if (val.castTag(.enum_field_index)) |field_index| { - switch (ty.tag()) { - .enum_simple => return @bitCast(i32, field_index.data), - .enum_full, .enum_nonexhaustive => { - const enum_full = ty.cast(Type.Payload.EnumFull).?.data; - if (enum_full.values.count() != 0) { - const tag_val = enum_full.values.keys()[field_index.data]; - return self.valueAsI32(tag_val, enum_full.tag_ty); - } else return @bitCast(i32, field_index.data); - }, - else => unreachable, - } - } else { - var int_tag_buffer: Type.Payload.Bits = undefined; - const int_tag_ty = ty.intTagType(&int_tag_buffer); - return self.valueAsI32(val, int_tag_ty); - } - }, - .Int => switch (ty.intInfo(self.target).signedness) { - .signed => return @truncate(i32, val.toSignedInt()), - .unsigned => return @bitCast(i32, @truncate(u32, val.toUnsignedInt())), - }, - .ErrorSet => { - const error_index = self.global_error_set.get(val.getError().?).?; - return @bitCast(i32, error_index); - }, - else => unreachable, // Programmer called this function for an illegal type - } - } - - fn airBlock(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; - const block_ty = try self.genBlockType(self.air.getRefType(ty_pl.ty)); - const extra = self.air.extraData(Air.Block, ty_pl.payload); - const body = self.air.extra[extra.end..][0..extra.data.body_len]; - - try self.startBlock(.block, block_ty, null); - // Here we set the current block idx, so breaks know the depth to jump - // to when breaking out. - try self.blocks.putNoClobber(self.gpa, inst, self.block_depth); - try self.genBody(body); - try self.endBlock(); - - return .none; - } - - /// appends a new wasm block to the code section and increases the `block_depth` by 1 - fn startBlock(self: *Context, block_type: wasm.Opcode, valtype: u8, with_offset: ?usize) !void { - self.block_depth += 1; - if (with_offset) |offset| { - try self.code.insert(offset, wasm.opcode(block_type)); - try self.code.insert(offset + 1, valtype); - } else { - try self.code.append(wasm.opcode(block_type)); - try self.code.append(valtype); - } - } - - /// Ends the current wasm block and decreases the `block_depth` by 1 - fn endBlock(self: *Context) !void { - try self.code.append(wasm.opcode(.end)); - self.block_depth -= 1; - } - - fn airLoop(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; - const loop = self.air.extraData(Air.Block, ty_pl.payload); - const body = self.air.extra[loop.end..][0..loop.data.body_len]; - - // result type of loop is always 'noreturn', meaning we can always - // emit the wasm type 'block_empty'. - try self.startBlock(.loop, wasm.block_empty, null); - try self.genBody(body); - - // breaking to the index of a loop block will continue the loop instead - try self.code.append(wasm.opcode(.br)); - try leb.writeULEB128(self.code.writer(), @as(u32, 0)); - - try self.endBlock(); - - return .none; - } - - fn airCondBr(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const pl_op = self.air.instructions.items(.data)[inst].pl_op; - const condition = self.resolveInst(pl_op.operand); - const extra = self.air.extraData(Air.CondBr, pl_op.payload); - const then_body = self.air.extra[extra.end..][0..extra.data.then_body_len]; - const else_body = self.air.extra[extra.end + then_body.len ..][0..extra.data.else_body_len]; - const writer = self.code.writer(); - // TODO: Handle death instructions for then and else body - - // insert blocks at the position of `offset` so - // the condition can jump to it - const offset = switch (condition) { - .code_offset => |offset| offset, - else => blk: { - const offset = self.code.items.len; - try self.emitWValue(condition); - break :blk offset; - }, - }; - - // result type is always noreturn, so use `block_empty` as type. - try self.startBlock(.block, wasm.block_empty, offset); - - // we inserted the block in front of the condition - // so now check if condition matches. If not, break outside this block - // and continue with the then codepath - try writer.writeByte(wasm.opcode(.br_if)); - try leb.writeULEB128(writer, @as(u32, 0)); - - try self.genBody(else_body); - try self.endBlock(); - - // Outer block that matches the condition - try self.genBody(then_body); - - return .none; - } - - fn airCmp(self: *Context, inst: Air.Inst.Index, op: std.math.CompareOperator) InnerError!WValue { - // save offset, so potential conditions can insert blocks in front of - // the comparison that we can later jump back to - const offset = self.code.items.len; - - const data: Air.Inst.Data = self.air.instructions.items(.data)[inst]; - const lhs = self.resolveInst(data.bin_op.lhs); - const rhs = self.resolveInst(data.bin_op.rhs); - const lhs_ty = self.air.typeOf(data.bin_op.lhs); - - try self.emitWValue(lhs); - try self.emitWValue(rhs); - - const signedness: std.builtin.Signedness = blk: { - // by default we tell the operand type is unsigned (i.e. bools and enum values) - if (lhs_ty.zigTypeTag() != .Int) break :blk .unsigned; - - // incase of an actual integer, we emit the correct signedness - break :blk lhs_ty.intInfo(self.target).signedness; - }; - const opcode: wasm.Opcode = buildOpcode(.{ - .valtype1 = try self.typeToValtype(lhs_ty), - .op = switch (op) { - .lt => .lt, - .lte => .le, - .eq => .eq, - .neq => .ne, - .gte => .ge, - .gt => .gt, - }, - .signedness = signedness, - }); - try self.code.append(wasm.opcode(opcode)); - return WValue{ .code_offset = offset }; - } - - fn airBr(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const br = self.air.instructions.items(.data)[inst].br; - - // if operand has codegen bits we should break with a value - if (self.air.typeOf(br.operand).hasCodeGenBits()) { - try self.emitWValue(self.resolveInst(br.operand)); - } - - // We map every block to its block index. - // We then determine how far we have to jump to it by subtracting it from current block depth - const idx: u32 = self.block_depth - self.blocks.get(br.block_inst).?; - const writer = self.code.writer(); - try writer.writeByte(wasm.opcode(.br)); - try leb.writeULEB128(writer, idx); - - return .none; - } - - fn airNot(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const offset = self.code.items.len; - - const operand = self.resolveInst(ty_op.operand); - try self.emitWValue(operand); - - // wasm does not have booleans nor the `not` instruction, therefore compare with 0 - // to create the same logic - const writer = self.code.writer(); - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeILEB128(writer, @as(i32, 0)); - - try writer.writeByte(wasm.opcode(.i32_eq)); - - return WValue{ .code_offset = offset }; - } - - fn airBreakpoint(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - _ = self; - _ = inst; - // unsupported by wasm itself. Can be implemented once we support DWARF - // for wasm - return .none; - } - - fn airUnreachable(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - _ = inst; - try self.code.append(wasm.opcode(.@"unreachable")); - return .none; - } - - fn airBitcast(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - return self.resolveInst(ty_op.operand); - } - - fn airStructFieldPtr(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; - const extra = self.air.extraData(Air.StructField, ty_pl.payload); - const struct_ptr = self.resolveInst(extra.data.struct_operand); - return structFieldPtr(struct_ptr, extra.data.field_index); - } - fn airStructFieldPtrIndex(self: *Context, inst: Air.Inst.Index, index: u32) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const struct_ptr = self.resolveInst(ty_op.operand); - return structFieldPtr(struct_ptr, index); - } - fn structFieldPtr(struct_ptr: WValue, index: u32) InnerError!WValue { - return WValue{ .local = struct_ptr.multi_value.index + index }; - } - - fn airStructFieldVal(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - if (self.liveness.isUnused(inst)) return WValue.none; - - const ty_pl = self.air.instructions.items(.data)[inst].ty_pl; - const extra = self.air.extraData(Air.StructField, ty_pl.payload).data; - const struct_multivalue = self.resolveInst(extra.struct_operand).multi_value; - return WValue{ .local = struct_multivalue.index + extra.field_index }; - } - - fn airSwitchBr(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - // result type is always 'noreturn' - const blocktype = wasm.block_empty; - const pl_op = self.air.instructions.items(.data)[inst].pl_op; - const target = self.resolveInst(pl_op.operand); - const target_ty = self.air.typeOf(pl_op.operand); - const switch_br = self.air.extraData(Air.SwitchBr, pl_op.payload); - var extra_index: usize = switch_br.end; - var case_i: u32 = 0; - - // a list that maps each value with its value and body based on the order inside the list. - const CaseValue = struct { integer: i32, value: Value }; - var case_list = try std.ArrayList(struct { - values: []const CaseValue, - body: []const Air.Inst.Index, - }).initCapacity(self.gpa, switch_br.data.cases_len); - defer for (case_list.items) |case| { - self.gpa.free(case.values); - } else case_list.deinit(); - - var lowest: i32 = 0; - var highest: i32 = 0; - while (case_i < switch_br.data.cases_len) : (case_i += 1) { - const case = self.air.extraData(Air.SwitchBr.Case, extra_index); - const items = @bitCast([]const Air.Inst.Ref, self.air.extra[case.end..][0..case.data.items_len]); - const case_body = self.air.extra[case.end + items.len ..][0..case.data.body_len]; - extra_index = case.end + items.len + case_body.len; - const values = try self.gpa.alloc(CaseValue, items.len); - errdefer self.gpa.free(values); - - for (items) |ref, i| { - const item_val = self.air.value(ref).?; - const int_val = self.valueAsI32(item_val, target_ty); - if (int_val < lowest) { - lowest = int_val; - } - if (int_val > highest) { - highest = int_val; - } - values[i] = .{ .integer = int_val, .value = item_val }; - } - - case_list.appendAssumeCapacity(.{ .values = values, .body = case_body }); - try self.startBlock(.block, blocktype, null); - } - - // When the highest and lowest values are seperated by '50', - // we define it as sparse and use an if/else-chain, rather than a jump table. - // When the target is an integer size larger than u32, we have no way to use the value - // as an index, therefore we also use an if/else-chain for those cases. - // TODO: Benchmark this to find a proper value, LLVM seems to draw the line at '40~45'. - const is_sparse = highest - lowest > 50 or target_ty.bitSize(self.target) > 32; - - const else_body = self.air.extra[extra_index..][0..switch_br.data.else_body_len]; - const has_else_body = else_body.len != 0; - if (has_else_body) { - try self.startBlock(.block, blocktype, null); - } - - if (!is_sparse) { - // Generate the jump table 'br_table' when the prongs are not sparse. - // The value 'target' represents the index into the table. - // Each index in the table represents a label to the branch - // to jump to. - try self.startBlock(.block, blocktype, null); - try self.emitWValue(target); - if (lowest < 0) { - // since br_table works using indexes, starting from '0', we must ensure all values - // we put inside, are atleast 0. - try self.code.append(wasm.opcode(.i32_const)); - try leb.writeILEB128(self.code.writer(), lowest * -1); - try self.code.append(wasm.opcode(.i32_add)); - } - try self.code.append(wasm.opcode(.br_table)); - const depth = highest - lowest + @boolToInt(has_else_body); - try leb.writeILEB128(self.code.writer(), depth); - while (lowest <= highest) : (lowest += 1) { - // idx represents the branch we jump to - const idx = blk: { - for (case_list.items) |case, idx| { - for (case.values) |case_value| { - if (case_value.integer == lowest) break :blk @intCast(u32, idx); - } - } - break :blk if (has_else_body) case_i else unreachable; - }; - try leb.writeULEB128(self.code.writer(), idx); - } else if (has_else_body) { - try leb.writeULEB128(self.code.writer(), @as(u32, case_i)); // default branch - } - try self.endBlock(); - } - - const signedness: std.builtin.Signedness = blk: { - // by default we tell the operand type is unsigned (i.e. bools and enum values) - if (target_ty.zigTypeTag() != .Int) break :blk .unsigned; - - // incase of an actual integer, we emit the correct signedness - break :blk target_ty.intInfo(self.target).signedness; - }; - - for (case_list.items) |case| { - // when sparse, we use if/else-chain, so emit conditional checks - if (is_sparse) { - // for single value prong we can emit a simple if - if (case.values.len == 1) { - try self.emitWValue(target); - try self.emitConstant(case.values[0].value, target_ty); - const opcode = buildOpcode(.{ - .valtype1 = try self.typeToValtype(target_ty), - .op = .ne, // not equal, because we want to jump out of this block if it does not match the condition. - .signedness = signedness, - }); - try self.code.append(wasm.opcode(opcode)); - try self.code.append(wasm.opcode(.br_if)); - try leb.writeULEB128(self.code.writer(), @as(u32, 0)); - } else { - // in multi-value prongs we must check if any prongs match the target value. - try self.startBlock(.block, blocktype, null); - for (case.values) |value| { - try self.emitWValue(target); - try self.emitConstant(value.value, target_ty); - const opcode = buildOpcode(.{ - .valtype1 = try self.typeToValtype(target_ty), - .op = .eq, - .signedness = signedness, - }); - try self.code.append(wasm.opcode(opcode)); - try self.code.append(wasm.opcode(.br_if)); - try leb.writeULEB128(self.code.writer(), @as(u32, 0)); - } - // value did not match any of the prong values - try self.code.append(wasm.opcode(.br)); - try leb.writeULEB128(self.code.writer(), @as(u32, 1)); - try self.endBlock(); - } - } - try self.genBody(case.body); - try self.endBlock(); - } - - if (has_else_body) { - try self.genBody(else_body); - try self.endBlock(); - } - return .none; - } - - fn airIsErr(self: *Context, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue { - const un_op = self.air.instructions.items(.data)[inst].un_op; - const operand = self.resolveInst(un_op); - const offset = self.code.items.len; - const writer = self.code.writer(); - - // load the error value which is positioned at multi_value's index - try self.emitWValue(.{ .local = operand.multi_value.index }); - // Compare the error value with '0' - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeILEB128(writer, @as(i32, 0)); - - try writer.writeByte(@enumToInt(opcode)); - - return WValue{ .code_offset = offset }; - } - - fn airUnwrapErrUnionPayload(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const operand = self.resolveInst(ty_op.operand); - // The index of multi_value contains the error code. To get the initial index of the payload we get - // the following index. Next, convert it to a `WValue.local` - // - // TODO: Check if payload is a type that requires a multi_value as well and emit that instead. i.e. a struct. - return WValue{ .local = operand.multi_value.index + 1 }; - } - - fn airWrapErrUnionPayload(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - return self.resolveInst(ty_op.operand); - } - - fn airIntcast(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const ty = self.air.getRefType(ty_op.ty); - const operand = self.resolveInst(ty_op.operand); - const ref_ty = self.air.typeOf(ty_op.operand); - const ref_info = ref_ty.intInfo(self.target); - const op_bits = ref_info.bits; - const wanted_bits = ty.intInfo(self.target).bits; - - try self.emitWValue(operand); - if (op_bits > 32 and wanted_bits <= 32) { - try self.code.append(wasm.opcode(.i32_wrap_i64)); - } else if (op_bits <= 32 and wanted_bits > 32) { - try self.code.append(wasm.opcode(switch (ref_info.signedness) { - .signed => .i64_extend_i32_s, - .unsigned => .i64_extend_i32_u, - })); - } - - // other cases are no-op - return .none; - } - - fn airIsNull(self: *Context, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue { - const un_op = self.air.instructions.items(.data)[inst].un_op; - const operand = self.resolveInst(un_op); - // const offset = self.code.items.len; - const writer = self.code.writer(); - - // load the null value which is positioned at multi_value's index - try self.emitWValue(.{ .local = operand.multi_value.index }); - // Compare the null value with '0' - try writer.writeByte(wasm.opcode(.i32_const)); - try leb.writeILEB128(writer, @as(i32, 0)); - - try writer.writeByte(@enumToInt(opcode)); - - // we save the result in a new local - const local = try self.allocLocal(Type.initTag(.i32)); - try writer.writeByte(wasm.opcode(.local_set)); - try leb.writeULEB128(writer, local.local); - - return local; - } - - fn airOptionalPayload(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const operand = self.resolveInst(ty_op.operand); - return WValue{ .local = operand.multi_value.index + 1 }; - } - - fn airOptionalPayloadPtrSet(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - const operand = self.resolveInst(ty_op.operand); - _ = operand; - return self.fail("TODO - wasm codegen for optional_payload_ptr_set", .{}); - } - - fn airWrapOptional(self: *Context, inst: Air.Inst.Index) InnerError!WValue { - const ty_op = self.air.instructions.items(.data)[inst].ty_op; - return self.resolveInst(ty_op.operand); - } -}; diff --git a/src/link/Wasm.zig b/src/link/Wasm.zig index a36850d9b8..a19d014dbe 100644 --- a/src/link/Wasm.zig +++ b/src/link/Wasm.zig @@ -12,7 +12,7 @@ const wasm = std.wasm; const Module = @import("../Module.zig"); const Compilation = @import("../Compilation.zig"); -const codegen = @import("../codegen/wasm.zig"); +const CodeGen = @import("../arch/wasm/CodeGen.zig"); const link = @import("../link.zig"); const trace = @import("../tracy.zig").trace; const build_options = @import("build_options"); @@ -54,6 +54,8 @@ offset_table_free_list: std.ArrayListUnmanaged(u32) = .{}, /// This is ment for bookkeeping so we can safely cleanup all codegen memory /// when calling `deinit` symbols: std.ArrayListUnmanaged(*Module.Decl) = .{}, +/// List of symbol indexes which are free to be used. +symbols_free_list: std.ArrayListUnmanaged(u32) = .{}, pub const FnData = struct { /// Generated code for the type of the function @@ -62,7 +64,8 @@ pub const FnData = struct { code: std.ArrayListUnmanaged(u8), /// Locations in the generated code where function indexes must be filled in. /// This must be kept ordered by offset. - idx_refs: std.ArrayListUnmanaged(struct { offset: u32, decl: *Module.Decl }), + /// `decl` is the symbol_index of the target. + idx_refs: std.ArrayListUnmanaged(struct { offset: u32, decl: u32 }), pub const empty: FnData = .{ .functype = .{}, @@ -156,7 +159,18 @@ pub fn deinit(self: *Wasm) void { if (build_options.have_llvm) { if (self.llvm_object) |llvm_object| llvm_object.destroy(self.base.allocator); } - for (self.symbols.items) |decl| { + + for (self.symbols.items) |decl, symbol_index| { + // Check if we already freed all memory for the symbol + // TODO: Audit this when we refactor the linker. + var already_freed = false; + for (self.symbols_free_list.items) |index| { + if (symbol_index == index) { + already_freed = true; + break; + } + } + if (already_freed) continue; decl.fn_link.wasm.functype.deinit(self.base.allocator); decl.fn_link.wasm.code.deinit(self.base.allocator); decl.fn_link.wasm.idx_refs.deinit(self.base.allocator); @@ -167,6 +181,7 @@ pub fn deinit(self: *Wasm) void { self.offset_table.deinit(self.base.allocator); self.offset_table_free_list.deinit(self.base.allocator); self.symbols.deinit(self.base.allocator); + self.symbols_free_list.deinit(self.base.allocator); } pub fn allocateDeclIndexes(self: *Wasm, decl: *Module.Decl) !void { @@ -178,9 +193,6 @@ pub fn allocateDeclIndexes(self: *Wasm, decl: *Module.Decl) !void { const block = &decl.link.wasm; block.init = true; - block.symbol_index = @intCast(u32, self.symbols.items.len); - self.symbols.appendAssumeCapacity(decl); - if (self.offset_table_free_list.popOrNull()) |index| { block.offset_index = index; } else { @@ -188,6 +200,14 @@ pub fn allocateDeclIndexes(self: *Wasm, decl: *Module.Decl) !void { _ = self.offset_table.addOneAssumeCapacity(); } + if (self.symbols_free_list.popOrNull()) |index| { + block.symbol_index = index; + self.symbols.items[block.symbol_index] = decl; + } else { + block.symbol_index = @intCast(u32, self.symbols.items.len); + self.symbols.appendAssumeCapacity(decl); + } + self.offset_table.items[block.offset_index] = 0; if (decl.ty.zigTypeTag() == .Fn) { @@ -215,7 +235,7 @@ pub fn updateFunc(self: *Wasm, module: *Module, func: *Module.Fn, air: Air, live fn_data.code.items.len = 0; fn_data.idx_refs.items.len = 0; - var context = codegen.Context{ + var codegen: CodeGen = .{ .gpa = self.base.allocator, .air = air, .liveness = liveness, @@ -226,20 +246,21 @@ pub fn updateFunc(self: *Wasm, module: *Module, func: *Module.Fn, air: Air, live .err_msg = undefined, .locals = .{}, .target = self.base.options.target, + .bin_file = &self.base, .global_error_set = self.base.options.module.?.global_error_set, }; - defer context.deinit(); + defer codegen.deinit(); // generate the 'code' section for the function declaration - const result = context.genFunc() catch |err| switch (err) { + const result = codegen.genFunc() catch |err| switch (err) { error.CodegenFail => { decl.analysis = .codegen_failure; - try module.failed_decls.put(module.gpa, decl, context.err_msg); + try module.failed_decls.put(module.gpa, decl, codegen.err_msg); return; }, else => |e| return e, }; - return self.finishUpdateDecl(decl, result, &context); + return self.finishUpdateDecl(decl, result, &codegen); } // Generate code for the Decl, storing it in memory to be later written to @@ -259,7 +280,7 @@ pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void { fn_data.code.items.len = 0; fn_data.idx_refs.items.len = 0; - var context = codegen.Context{ + var codegen: CodeGen = .{ .gpa = self.base.allocator, .air = undefined, .liveness = undefined, @@ -270,28 +291,29 @@ pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void { .err_msg = undefined, .locals = .{}, .target = self.base.options.target, + .bin_file = &self.base, .global_error_set = self.base.options.module.?.global_error_set, }; - defer context.deinit(); + defer codegen.deinit(); // generate the 'code' section for the function declaration - const result = context.gen(decl.ty, decl.val) catch |err| switch (err) { + const result = codegen.gen(decl.ty, decl.val) catch |err| switch (err) { error.CodegenFail => { decl.analysis = .codegen_failure; - try module.failed_decls.put(module.gpa, decl, context.err_msg); + try module.failed_decls.put(module.gpa, decl, codegen.err_msg); return; }, else => |e| return e, }; - return self.finishUpdateDecl(decl, result, &context); + return self.finishUpdateDecl(decl, result, &codegen); } -fn finishUpdateDecl(self: *Wasm, decl: *Module.Decl, result: codegen.Result, context: *codegen.Context) !void { +fn finishUpdateDecl(self: *Wasm, decl: *Module.Decl, result: CodeGen.Result, codegen: *CodeGen) !void { const fn_data: *FnData = &decl.fn_link.wasm; - fn_data.code = context.code.toUnmanaged(); - fn_data.functype = context.func_type_data.toUnmanaged(); + fn_data.code = codegen.code.toUnmanaged(); + fn_data.functype = codegen.func_type_data.toUnmanaged(); const code: []const u8 = switch (result) { .appended => @as([]const u8, fn_data.code.items), @@ -299,14 +321,7 @@ fn finishUpdateDecl(self: *Wasm, decl: *Module.Decl, result: codegen.Result, con }; const block = &decl.link.wasm; - if (decl.ty.zigTypeTag() == .Fn) { - // as locals are patched afterwards, the offsets of funcidx's are off, - // here we update them to correct them - for (fn_data.idx_refs.items) |*func| { - // For each local, add 6 bytes (count + type) - func.offset += @intCast(u32, context.locals.items.len * 6); - } - } else { + if (decl.ty.zigTypeTag() != .Fn) { block.size = @intCast(u32, code.len); block.data = code.ptr; } @@ -359,18 +374,13 @@ pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void { block.unplug(); self.offset_table_free_list.append(self.base.allocator, decl.link.wasm.offset_index) catch {}; - _ = self.symbols.swapRemove(block.symbol_index); - - // update symbol_index as we swap removed the last symbol into the removed's position - if (block.symbol_index < self.symbols.items.len) - self.symbols.items[block.symbol_index].link.wasm.symbol_index = block.symbol_index; + self.symbols_free_list.append(self.base.allocator, block.symbol_index) catch {}; block.init = false; decl.fn_link.wasm.functype.deinit(self.base.allocator); decl.fn_link.wasm.code.deinit(self.base.allocator); decl.fn_link.wasm.idx_refs.deinit(self.base.allocator); - decl.fn_link.wasm = undefined; } pub fn flush(self: *Wasm, comp: *Compilation) !void { @@ -553,18 +563,12 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void { // Write the already generated code to the file, inserting // function indexes where required. - var current: u32 = 0; for (fn_data.idx_refs.items) |idx_ref| { - try writer.writeAll(fn_data.code.items[current..idx_ref.offset]); - current = idx_ref.offset; - // Use a fixed width here to make calculating the code size - // in codegen.wasm.gen() simpler. - var buf: [5]u8 = undefined; - leb.writeUnsignedFixed(5, &buf, self.getFuncidx(idx_ref.decl).?); - try writer.writeAll(&buf); + const relocatable_decl = self.symbols.items[idx_ref.decl]; + const index = self.getFuncidx(relocatable_decl).?; + leb.writeUnsignedFixed(5, fn_data.code.items[idx_ref.offset..][0..5], index); } - - try writer.writeAll(fn_data.code.items[current..]); + try writer.writeAll(fn_data.code.items); } try writeVecSectionHeader( file,