mirror of
https://github.com/ziglang/zig.git
synced 2026-02-20 16:24:51 +00:00
Merge pull request #10240 from Luukdegram/stage2-wasm-behaviour
Stage2: wasm - Implement 'zig test'
This commit is contained in:
commit
7a7df392d1
@ -30,6 +30,8 @@ comptime {
|
||||
}
|
||||
} else if (builtin.os.tag == .windows) {
|
||||
@export(wWinMainCRTStartup2, .{ .name = "wWinMainCRTStartup" });
|
||||
} else if (builtin.os.tag == .wasi and @hasDecl(root, "main")) {
|
||||
@export(wasmMain2, .{ .name = "_start" });
|
||||
} else {
|
||||
if (!@hasDecl(root, "_start")) {
|
||||
@export(_start2, .{ .name = "_start" });
|
||||
@ -98,6 +100,22 @@ fn callMain2() noreturn {
|
||||
exit2(0);
|
||||
}
|
||||
|
||||
fn wasmMain2() u8 {
|
||||
switch (@typeInfo(@typeInfo(@TypeOf(root.main)).Fn.return_type.?)) {
|
||||
.Void => {
|
||||
root.main();
|
||||
return 0;
|
||||
},
|
||||
.Int => |info| {
|
||||
if (info.bits != 8 or info.signedness == .signed) {
|
||||
@compileError(bad_main_ret);
|
||||
}
|
||||
return root.main();
|
||||
},
|
||||
else => @compileError("Bad return type main"),
|
||||
}
|
||||
}
|
||||
|
||||
fn wWinMainCRTStartup2() callconv(.C) noreturn {
|
||||
root.main();
|
||||
exit2(0);
|
||||
|
||||
@ -692,6 +692,7 @@ fn typeToValtype(self: *Self, ty: Type) InnerError!wasm.Valtype {
|
||||
.Struct,
|
||||
.ErrorUnion,
|
||||
.Optional,
|
||||
.Fn,
|
||||
=> wasm.Valtype.i32,
|
||||
else => self.fail("TODO - Wasm valtype for type '{}'", .{ty}),
|
||||
};
|
||||
@ -809,23 +810,52 @@ pub fn genFunc(self: *Self) InnerError!Result {
|
||||
}
|
||||
|
||||
/// Generates the wasm bytecode for the declaration belonging to `Context`
|
||||
pub fn gen(self: *Self, ty: Type, val: Value) InnerError!Result {
|
||||
pub fn genDecl(self: *Self, ty: Type, val: Value) InnerError!Result {
|
||||
if (val.isUndef()) {
|
||||
try self.code.appendNTimes(0xaa, @intCast(usize, ty.abiSize(self.target)));
|
||||
return Result.appended;
|
||||
}
|
||||
switch (ty.zigTypeTag()) {
|
||||
.Fn => {
|
||||
if (val.tag() == .extern_fn) {
|
||||
var func_type = try self.genFunctype(self.decl.ty);
|
||||
defer func_type.deinit(self.gpa);
|
||||
self.decl.fn_link.wasm.type_index = try self.bin_file.putOrGetFuncType(func_type);
|
||||
return Result.appended; // don't need code body for extern functions
|
||||
}
|
||||
return self.fail("TODO implement wasm codegen for function pointers", .{});
|
||||
const fn_decl = switch (val.tag()) {
|
||||
.extern_fn => val.castTag(.extern_fn).?.data,
|
||||
.function => val.castTag(.function).?.data.owner_decl,
|
||||
else => unreachable,
|
||||
};
|
||||
return try self.lowerDeclRef(fn_decl);
|
||||
},
|
||||
.Array => {
|
||||
if (val.castTag(.bytes)) |payload| {
|
||||
.Optional => {
|
||||
var opt_buf: Type.Payload.ElemType = undefined;
|
||||
const payload_type = ty.optionalChild(&opt_buf);
|
||||
if (ty.isPtrLikeOptional()) {
|
||||
if (val.castTag(.opt_payload)) |payload| {
|
||||
return try self.genDecl(payload_type, payload.data);
|
||||
} else if (!val.isNull()) {
|
||||
return try self.genDecl(payload_type, val);
|
||||
} else {
|
||||
try self.code.appendNTimes(0, @intCast(usize, ty.abiSize(self.target)));
|
||||
return Result.appended;
|
||||
}
|
||||
}
|
||||
// `null-tag` byte
|
||||
try self.code.appendNTimes(@boolToInt(!val.isNull()), 4);
|
||||
const pl_result = try self.genDecl(
|
||||
payload_type,
|
||||
if (val.castTag(.opt_payload)) |pl| pl.data else Value.initTag(.undef),
|
||||
);
|
||||
switch (pl_result) {
|
||||
.appended => {},
|
||||
.externally_managed => |payload| try self.code.appendSlice(payload),
|
||||
}
|
||||
return Result.appended;
|
||||
},
|
||||
.Array => switch (val.tag()) {
|
||||
.bytes => {
|
||||
const payload = val.castTag(.bytes).?;
|
||||
if (ty.sentinel()) |sentinel| {
|
||||
try self.code.appendSlice(payload.data);
|
||||
|
||||
switch (try self.gen(ty.childType(), sentinel)) {
|
||||
switch (try self.genDecl(ty.childType(), sentinel)) {
|
||||
.appended => return Result.appended,
|
||||
.externally_managed => |data| {
|
||||
try self.code.appendSlice(data);
|
||||
@ -834,16 +864,33 @@ pub fn gen(self: *Self, ty: Type, val: Value) InnerError!Result {
|
||||
}
|
||||
}
|
||||
return Result{ .externally_managed = payload.data };
|
||||
} else return self.fail("TODO implement gen for more kinds of arrays", .{});
|
||||
},
|
||||
.array => {
|
||||
const elem_vals = val.castTag(.array).?.data;
|
||||
const elem_ty = ty.elemType();
|
||||
for (elem_vals) |elem_val| {
|
||||
switch (try self.genDecl(elem_ty, elem_val)) {
|
||||
.appended => {},
|
||||
.externally_managed => |data| {
|
||||
try self.code.appendSlice(data);
|
||||
},
|
||||
}
|
||||
}
|
||||
return Result.appended;
|
||||
},
|
||||
else => return self.fail("TODO implement genDecl for array type value: {s}", .{@tagName(val.tag())}),
|
||||
},
|
||||
.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});
|
||||
const abi_size = @intCast(usize, ty.abiSize(self.target));
|
||||
// todo: Implement integer sizes larger than 64bits
|
||||
if (info.bits > 64) return self.fail("TODO: Implement genDecl for integer bit size: {d}", .{info.bits});
|
||||
var buf: [8]u8 = undefined;
|
||||
if (info.signedness == .unsigned) {
|
||||
std.mem.writeIntLittle(u64, &buf, val.toUnsignedInt());
|
||||
} else std.mem.writeIntLittle(i64, &buf, val.toSignedInt());
|
||||
try self.code.appendSlice(buf[0..abi_size]);
|
||||
return Result.appended;
|
||||
},
|
||||
.Enum => {
|
||||
try self.emitConstant(val, ty);
|
||||
@ -855,15 +902,83 @@ pub fn gen(self: *Self, ty: Type, val: Value) InnerError!Result {
|
||||
return Result.appended;
|
||||
},
|
||||
.Struct => {
|
||||
// TODO write the fields for real
|
||||
const abi_size = try std.math.cast(usize, ty.abiSize(self.target));
|
||||
try self.code.writer().writeByteNTimes(0xaa, abi_size);
|
||||
return Result{ .appended = {} };
|
||||
const field_vals = val.castTag(.@"struct").?.data;
|
||||
for (field_vals) |field_val, index| {
|
||||
const field_ty = ty.structFieldType(index);
|
||||
if (!field_ty.hasCodeGenBits()) continue;
|
||||
|
||||
switch (try self.genDecl(field_ty, field_val)) {
|
||||
.appended => {},
|
||||
.externally_managed => |payload| try self.code.appendSlice(payload),
|
||||
}
|
||||
}
|
||||
return Result.appended;
|
||||
},
|
||||
.Union => {
|
||||
// TODO: Implement Union declarations
|
||||
const abi_size = @intCast(usize, ty.abiSize(self.target));
|
||||
try self.code.appendNTimes(0xaa, abi_size);
|
||||
return Result.appended;
|
||||
},
|
||||
.Pointer => switch (val.tag()) {
|
||||
.variable => {
|
||||
const decl = val.castTag(.variable).?.data.owner_decl;
|
||||
return try self.lowerDeclRef(decl);
|
||||
},
|
||||
.decl_ref => {
|
||||
const decl = val.castTag(.decl_ref).?.data;
|
||||
return try self.lowerDeclRef(decl);
|
||||
},
|
||||
.slice => {
|
||||
const slice = val.castTag(.slice).?.data;
|
||||
var buf: Type.SlicePtrFieldTypeBuffer = undefined;
|
||||
const ptr_ty = ty.slicePtrFieldType(&buf);
|
||||
switch (try self.genDecl(ptr_ty, slice.ptr)) {
|
||||
.externally_managed => |data| try self.code.appendSlice(data),
|
||||
.appended => {},
|
||||
}
|
||||
switch (try self.genDecl(Type.usize, slice.len)) {
|
||||
.externally_managed => |data| try self.code.appendSlice(data),
|
||||
.appended => {},
|
||||
}
|
||||
return Result.appended;
|
||||
},
|
||||
else => return self.fail("TODO: Implement zig decl gen for pointer type value: '{s}'", .{@tagName(val.tag())}),
|
||||
},
|
||||
else => |tag| return self.fail("TODO: Implement zig type codegen for type: '{s}'", .{tag}),
|
||||
}
|
||||
}
|
||||
|
||||
fn lowerDeclRef(self: *Self, decl: *Module.Decl) InnerError!Result {
|
||||
decl.alive = true;
|
||||
|
||||
const offset = @intCast(u32, self.code.items.len);
|
||||
const atom = &self.decl.link.wasm;
|
||||
const target_sym_index = decl.link.wasm.sym_index;
|
||||
|
||||
if (decl.ty.zigTypeTag() == .Fn) {
|
||||
// We found a function pointer, so add it to our table,
|
||||
// as function pointers are not allowed to be stored inside the data section,
|
||||
// but rather in a function table which are called by index
|
||||
try self.bin_file.addTableFunction(target_sym_index);
|
||||
try atom.relocs.append(self.gpa, .{
|
||||
.index = target_sym_index,
|
||||
.offset = offset,
|
||||
.relocation_type = .R_WASM_TABLE_INDEX_I32,
|
||||
});
|
||||
} else {
|
||||
try atom.relocs.append(self.gpa, .{
|
||||
.index = target_sym_index,
|
||||
.offset = offset,
|
||||
.relocation_type = .R_WASM_MEMORY_ADDR_I32,
|
||||
});
|
||||
}
|
||||
const ptr_width = @intCast(usize, self.target.cpu.arch.ptrBitWidth() / 8);
|
||||
try self.code.appendNTimes(0xaa, ptr_width);
|
||||
|
||||
return Result.appended;
|
||||
}
|
||||
|
||||
const CallWValues = struct {
|
||||
args: []WValue,
|
||||
return_value: WValue,
|
||||
@ -1015,6 +1130,10 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
|
||||
.loop => self.airLoop(inst),
|
||||
.not => self.airNot(inst),
|
||||
.ret => self.airRet(inst),
|
||||
.ret_ptr => self.airRetPtr(inst),
|
||||
.ret_load => self.airRetLoad(inst),
|
||||
.slice_len => self.airSliceLen(inst),
|
||||
.slice_elem_val => self.airSliceElemVal(inst),
|
||||
.store => self.airStore(inst),
|
||||
.struct_field_ptr => self.airStructFieldPtr(inst),
|
||||
.struct_field_ptr_index_0 => self.airStructFieldPtrIndex(inst, 0),
|
||||
@ -1027,6 +1146,7 @@ fn genInst(self: *Self, inst: Air.Inst.Index) !WValue {
|
||||
.wrap_optional => self.airWrapOptional(inst),
|
||||
|
||||
.unwrap_errunion_payload => self.airUnwrapErrUnionPayload(inst),
|
||||
.unwrap_errunion_err => self.airUnwrapErrUnionError(inst),
|
||||
.wrap_errunion_payload => self.airWrapErrUnionPayload(inst),
|
||||
|
||||
.optional_payload => self.airOptionalPayload(inst),
|
||||
@ -1059,13 +1179,48 @@ fn airRet(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
return .none;
|
||||
}
|
||||
|
||||
fn airRetPtr(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
const child_type = self.air.typeOfIndex(inst).childType();
|
||||
|
||||
// Initialize the stack
|
||||
if (self.initial_stack_value == .none) {
|
||||
try self.initializeStack();
|
||||
}
|
||||
|
||||
const abi_size = child_type.abiSize(self.target);
|
||||
if (abi_size == 0) return WValue{ .none = {} };
|
||||
|
||||
// local, containing the offset to the stack position
|
||||
const local = try self.allocLocal(Type.initTag(.i32)); // always pointer therefore i32
|
||||
try self.moveStack(@intCast(u32, abi_size), local.local);
|
||||
|
||||
return local;
|
||||
}
|
||||
|
||||
fn airRetLoad(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);
|
||||
const result = try self.load(operand, self.air.typeOf(un_op), 0);
|
||||
try self.addLabel(.local_get, result.local);
|
||||
try self.restoreStackPointer();
|
||||
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 ty = self.air.typeOf(pl_op.operand);
|
||||
|
||||
const target: *Decl = blk: {
|
||||
const func_val = self.air.value(pl_op.operand).?;
|
||||
const fn_ty = switch (ty.zigTypeTag()) {
|
||||
.Fn => ty,
|
||||
.Pointer => ty.childType(),
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
const target: ?*Decl = blk: {
|
||||
const func_val = self.air.value(pl_op.operand) orelse break :blk null;
|
||||
|
||||
if (func_val.castTag(.function)) |func| {
|
||||
break :blk func.data.owner_decl;
|
||||
@ -1080,9 +1235,25 @@ fn airCall(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
try self.emitWValue(arg_val);
|
||||
}
|
||||
|
||||
try self.addLabel(.call, target.link.wasm.sym_index);
|
||||
if (target) |direct| {
|
||||
try self.addLabel(.call, direct.link.wasm.sym_index);
|
||||
} else {
|
||||
// in this case we call a function pointer
|
||||
// so load its value onto the stack
|
||||
std.debug.assert(ty.zigTypeTag() == .Pointer);
|
||||
const operand = self.resolveInst(pl_op.operand);
|
||||
try self.emitWValue(operand);
|
||||
const result = try self.load(operand, fn_ty, operand.local_with_offset.offset);
|
||||
try self.addLabel(.local_get, result.local);
|
||||
|
||||
const ret_ty = target.ty.fnReturnType();
|
||||
var fn_type = try self.genFunctype(fn_ty);
|
||||
defer fn_type.deinit(self.gpa);
|
||||
|
||||
const fn_type_index = try self.bin_file.putOrGetFuncType(fn_type);
|
||||
try self.addLabel(.call_indirect, fn_type_index);
|
||||
}
|
||||
|
||||
const ret_ty = fn_ty.fnReturnType();
|
||||
switch (ret_ty.zigTypeTag()) {
|
||||
.Void, .NoReturn => return WValue.none,
|
||||
else => {
|
||||
@ -1145,13 +1316,16 @@ fn store(self: *Self, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerErro
|
||||
// in memory
|
||||
try self.emitWValue(rhs);
|
||||
const tag_local = try self.allocLocal(tag_ty);
|
||||
const payload_local = try self.allocLocal(payload_ty);
|
||||
|
||||
try self.addLabel(.local_set, payload_local.local);
|
||||
if (payload_ty.hasCodeGenBits()) {
|
||||
const payload_local = try self.allocLocal(payload_ty);
|
||||
try self.addLabel(.local_set, payload_local.local);
|
||||
try self.store(lhs, payload_local, payload_ty, payload_offset);
|
||||
}
|
||||
try self.addLabel(.local_set, tag_local.local);
|
||||
|
||||
try self.store(lhs, tag_local, tag_ty, 0);
|
||||
return try self.store(lhs, payload_local, payload_ty, payload_offset);
|
||||
return;
|
||||
},
|
||||
.local => {
|
||||
// Load values from `rhs` stack position and store in `lhs` instead
|
||||
@ -1197,9 +1371,18 @@ fn store(self: *Self, lhs: WValue, rhs: WValue, ty: Type, offset: u32) InnerErro
|
||||
try self.emitWValue(lhs);
|
||||
try self.emitWValue(rhs);
|
||||
const valtype = try self.typeToValtype(ty);
|
||||
// check if we should pass by pointer or value based on ABI size
|
||||
// TODO: Implement a way to get ABI values from a given type,
|
||||
// that is portable across the backend, rather than copying logic.
|
||||
const abi_size = if ((ty.isInt() or ty.isAnyFloat()) and ty.abiSize(self.target) <= 8)
|
||||
@intCast(u8, ty.abiSize(self.target))
|
||||
else if (ty.zigTypeTag() == .ErrorSet or ty.zigTypeTag() == .Enum)
|
||||
@intCast(u8, ty.abiSize(self.target))
|
||||
else
|
||||
@as(u8, 4);
|
||||
const opcode = buildOpcode(.{
|
||||
.valtype1 = valtype,
|
||||
.width = @intCast(u8, Type.abiSize(ty, self.target) * 8), // use bitsize instead of byte size
|
||||
.width = abi_size * 8, // use bitsize instead of byte size
|
||||
.op = .store,
|
||||
});
|
||||
|
||||
@ -1220,7 +1403,7 @@ fn airLoad(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
const ty = self.air.getRefType(ty_op.ty);
|
||||
|
||||
return switch (ty.zigTypeTag()) {
|
||||
.Struct, .ErrorUnion, .Optional => operand, // pass as pointer
|
||||
.Struct, .ErrorUnion, .Optional, .Pointer => operand, // pass as pointer
|
||||
else => switch (operand) {
|
||||
.local_with_offset => |with_offset| try self.load(operand, ty, with_offset.offset),
|
||||
else => try self.load(operand, ty, 0),
|
||||
@ -1233,9 +1416,19 @@ fn load(self: *Self, operand: WValue, ty: Type, offset: u32) InnerError!WValue {
|
||||
try self.emitWValue(operand);
|
||||
// Build the opcode with the right bitsize
|
||||
const signedness: std.builtin.Signedness = if (ty.isUnsignedInt()) .unsigned else .signed;
|
||||
// check if we should pass by pointer or value based on ABI size
|
||||
// TODO: Implement a way to get ABI values from a given type,
|
||||
// that is portable across the backend, rather than copying logic.
|
||||
const abi_size = if ((ty.isInt() or ty.isAnyFloat()) and ty.abiSize(self.target) <= 8)
|
||||
@intCast(u8, ty.abiSize(self.target))
|
||||
else if (ty.zigTypeTag() == .ErrorSet or ty.zigTypeTag() == .Enum)
|
||||
@intCast(u8, ty.abiSize(self.target))
|
||||
else
|
||||
@as(u8, 4);
|
||||
|
||||
const opcode = buildOpcode(.{
|
||||
.valtype1 = try self.typeToValtype(ty),
|
||||
.width = @intCast(u8, Type.abiSize(ty, self.target) * 8), // use bitsize instead of byte size
|
||||
.width = abi_size * 8, // use bitsize instead of byte size
|
||||
.op = .load,
|
||||
.signedness = signedness,
|
||||
});
|
||||
@ -1399,14 +1592,19 @@ fn emitConstant(self: *Self, val: Value, ty: Type) InnerError!void {
|
||||
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);
|
||||
|
||||
if (payload_type.hasCodeGenBits()) {
|
||||
// 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);
|
||||
if (payload_type.hasCodeGenBits()) {
|
||||
// no payload, so write a '0' const
|
||||
try self.addImm32(0);
|
||||
}
|
||||
}
|
||||
},
|
||||
.Optional => {
|
||||
@ -1867,8 +2065,19 @@ 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);
|
||||
const err_ty = self.air.typeOf(ty_op.operand);
|
||||
const payload_ty = err_ty.errorUnionPayload();
|
||||
if (!payload_ty.hasCodeGenBits()) return WValue.none;
|
||||
const offset = @intCast(u32, err_ty.errorUnionSet().abiSize(self.target));
|
||||
return self.load(operand, err_ty.errorUnionPayload(), offset);
|
||||
return try self.load(operand, payload_ty, offset);
|
||||
}
|
||||
|
||||
fn airUnwrapErrUnionError(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
if (self.liveness.isUnused(inst)) return WValue.none;
|
||||
|
||||
const ty_op = self.air.instructions.items(.data)[inst].ty_op;
|
||||
const operand = self.resolveInst(ty_op.operand);
|
||||
const err_ty = self.air.typeOf(ty_op.operand);
|
||||
return try self.load(operand, err_ty.errorUnionSet(), 0);
|
||||
}
|
||||
|
||||
fn airWrapErrUnionPayload(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
@ -1886,18 +2095,20 @@ fn airIntcast(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
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.emitWValue(operand);
|
||||
try self.addTag(.i32_wrap_i64);
|
||||
} else if (op_bits <= 32 and wanted_bits > 32) {
|
||||
try self.emitWValue(operand);
|
||||
try self.addTag(switch (ref_info.signedness) {
|
||||
.signed => .i64_extend_i32_s,
|
||||
.unsigned => .i64_extend_i32_u,
|
||||
});
|
||||
}
|
||||
} else return operand;
|
||||
|
||||
// other cases are no-op
|
||||
return .none;
|
||||
const result = try self.allocLocal(ty);
|
||||
try self.addLabel(.local_set, result.local);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn airIsNull(self: *Self, inst: Air.Inst.Index, opcode: wasm.Opcode) InnerError!WValue {
|
||||
@ -1961,3 +2172,67 @@ fn airWrapOptional(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
.offset = @intCast(u32, offset),
|
||||
} };
|
||||
}
|
||||
|
||||
fn airSliceLen(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
if (self.liveness.isUnused(inst)) return WValue.none;
|
||||
|
||||
const ty_op = self.air.instructions.items(.data)[inst].ty_op;
|
||||
const operand = self.resolveInst(ty_op.operand);
|
||||
const pointer_width = self.target.cpu.arch.ptrBitWidth() / 8;
|
||||
|
||||
// Get pointer to slice
|
||||
try self.emitWValue(operand);
|
||||
// length of slice is stored after the pointer of the slice
|
||||
const extra_index = try self.addExtra(Mir.MemArg{
|
||||
.offset = pointer_width,
|
||||
.alignment = pointer_width,
|
||||
});
|
||||
try self.addInst(.{ .tag = .i32_load, .data = .{ .payload = extra_index } });
|
||||
|
||||
const result = try self.allocLocal(Type.initTag(.i32)); // pointer is always i32
|
||||
// store slice length in local
|
||||
try self.addLabel(.local_set, result.local);
|
||||
return result;
|
||||
}
|
||||
|
||||
fn airSliceElemVal(self: *Self, inst: Air.Inst.Index) InnerError!WValue {
|
||||
if (self.liveness.isUnused(inst)) return WValue.none;
|
||||
|
||||
const bin_op = self.air.instructions.items(.data)[inst].bin_op;
|
||||
const slice_ty = self.air.typeOf(bin_op.lhs);
|
||||
const slice = self.resolveInst(bin_op.lhs);
|
||||
const index = self.resolveInst(bin_op.rhs);
|
||||
const elem_ty = slice_ty.childType();
|
||||
const elem_size = elem_ty.abiSize(self.target);
|
||||
|
||||
// load pointer onto stack
|
||||
try self.emitWValue(slice);
|
||||
|
||||
// calculate index into slice
|
||||
try self.emitWValue(index);
|
||||
try self.addImm32(@bitCast(i32, @intCast(u32, elem_size)));
|
||||
try self.addTag(.i32_mul);
|
||||
try self.addTag(.i32_add);
|
||||
|
||||
const abi_size = if (elem_size < 8)
|
||||
@intCast(u8, elem_size)
|
||||
else
|
||||
@as(u8, 4); // elements larger than 8 bytes will be passed by pointer
|
||||
|
||||
const extra_index = try self.addExtra(Mir.MemArg{
|
||||
.offset = 0,
|
||||
.alignment = elem_ty.abiAlignment(self.target),
|
||||
});
|
||||
const signedness: std.builtin.Signedness = if (elem_ty.isUnsignedInt()) .unsigned else .signed;
|
||||
const opcode = buildOpcode(.{
|
||||
.valtype1 = try self.typeToValtype(elem_ty),
|
||||
.width = abi_size * 8,
|
||||
.op = .load,
|
||||
.signedness = signedness,
|
||||
});
|
||||
try self.addInst(.{ .tag = Mir.Inst.Tag.fromOpcode(opcode), .data = .{ .payload = extra_index } });
|
||||
|
||||
const result = try self.allocLocal(elem_ty);
|
||||
try self.addLabel(.local_set, result.local);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -47,6 +47,7 @@ pub fn emitMir(emit: *Emit) InnerError!void {
|
||||
|
||||
// relocatables
|
||||
.call => try emit.emitCall(inst),
|
||||
.call_indirect => try emit.emitCallIndirect(inst),
|
||||
.global_get => try emit.emitGlobal(tag, inst),
|
||||
.global_set => try emit.emitGlobal(tag, inst),
|
||||
.memory_address => try emit.emitMemAddress(inst),
|
||||
@ -256,7 +257,7 @@ fn emitMemArg(emit: *Emit, tag: Mir.Inst.Tag, inst: Mir.Inst.Index) !void {
|
||||
try emit.code.append(@enumToInt(tag));
|
||||
|
||||
// wasm encodes alignment as power of 2, rather than natural alignment
|
||||
const encoded_alignment = mem_arg.alignment >> 1;
|
||||
const encoded_alignment = @ctz(u32, mem_arg.alignment);
|
||||
try leb128.writeULEB128(emit.code.writer(), encoded_alignment);
|
||||
try leb128.writeULEB128(emit.code.writer(), mem_arg.offset);
|
||||
}
|
||||
@ -276,6 +277,13 @@ fn emitCall(emit: *Emit, inst: Mir.Inst.Index) !void {
|
||||
});
|
||||
}
|
||||
|
||||
fn emitCallIndirect(emit: *Emit, inst: Mir.Inst.Index) !void {
|
||||
const label = emit.mir.instructions.items(.data)[inst].label;
|
||||
try emit.code.append(std.wasm.opcode(.call_indirect));
|
||||
try leb128.writeULEB128(emit.code.writer(), @as(u32, 0)); // TODO: Emit relocation for table index
|
||||
try leb128.writeULEB128(emit.code.writer(), label);
|
||||
}
|
||||
|
||||
fn emitMemAddress(emit: *Emit, inst: Mir.Inst.Index) !void {
|
||||
const symbol_index = emit.mir.instructions.items(.data)[inst].label;
|
||||
try emit.code.append(std.wasm.opcode(.i32_const));
|
||||
|
||||
@ -69,6 +69,11 @@ pub const Inst = struct {
|
||||
///
|
||||
/// Uses `label`
|
||||
call = 0x10,
|
||||
/// Calls a function pointer by its function signature
|
||||
/// and index into the function table.
|
||||
///
|
||||
/// Uses `label`
|
||||
call_indirect = 0x11,
|
||||
/// Loads a local at given index onto the stack.
|
||||
///
|
||||
/// Uses `label`
|
||||
|
||||
@ -79,7 +79,9 @@ memories: wasm.Memory = .{ .limits = .{ .min = 0, .max = null } },
|
||||
/// Indirect function table, used to call function pointers
|
||||
/// When this is non-zero, we must emit a table entry,
|
||||
/// as well as an 'elements' section.
|
||||
function_table: std.ArrayListUnmanaged(Symbol) = .{},
|
||||
///
|
||||
/// Note: Key is symbol index, value represents the index into the table
|
||||
function_table: std.AutoHashMapUnmanaged(u32, u32) = .{},
|
||||
|
||||
pub const Segment = struct {
|
||||
alignment: u32,
|
||||
@ -276,7 +278,7 @@ pub fn updateDecl(self: *Wasm, module: *Module, decl: *Module.Decl) !void {
|
||||
defer codegen.deinit();
|
||||
|
||||
// generate the 'code' section for the function declaration
|
||||
const result = codegen.gen(decl.ty, decl.val) catch |err| switch (err) {
|
||||
const result = codegen.genDecl(decl.ty, decl.val) catch |err| switch (err) {
|
||||
error.CodegenFail => {
|
||||
decl.analysis = .codegen_failure;
|
||||
try module.failed_decls.put(module.gpa, decl, codegen.err_msg);
|
||||
@ -334,6 +336,25 @@ pub fn freeDecl(self: *Wasm, decl: *Module.Decl) void {
|
||||
else => unreachable,
|
||||
}
|
||||
}
|
||||
|
||||
// maybe remove from function table if needed
|
||||
if (decl.ty.zigTypeTag() == .Fn) {
|
||||
_ = self.function_table.remove(atom.sym_index);
|
||||
}
|
||||
}
|
||||
|
||||
/// Appends a new entry to the indirect function table
|
||||
pub fn addTableFunction(self: *Wasm, symbol_index: u32) !void {
|
||||
const index = @intCast(u32, self.function_table.count());
|
||||
try self.function_table.put(self.base.allocator, symbol_index, index);
|
||||
}
|
||||
|
||||
fn mapFunctionTable(self: *Wasm) void {
|
||||
var it = self.function_table.valueIterator();
|
||||
var index: u32 = 0;
|
||||
while (it.next()) |value_ptr| : (index += 1) {
|
||||
value_ptr.* = index;
|
||||
}
|
||||
}
|
||||
|
||||
fn addOrUpdateImport(self: *Wasm, decl: *Module.Decl) !void {
|
||||
@ -583,6 +604,7 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
|
||||
|
||||
try self.setupMemory();
|
||||
try self.allocateAtoms();
|
||||
self.mapFunctionTable();
|
||||
|
||||
const file = self.base.file.?;
|
||||
const header_size = 5 + 1;
|
||||
@ -662,6 +684,22 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
|
||||
);
|
||||
}
|
||||
|
||||
if (self.function_table.count() > 0) {
|
||||
const header_offset = try reserveVecSectionHeader(file);
|
||||
const writer = file.writer();
|
||||
|
||||
try leb.writeULEB128(writer, wasm.reftype(.funcref));
|
||||
try emitLimits(writer, .{ .min = 1, .max = null });
|
||||
|
||||
try writeVecSectionHeader(
|
||||
file,
|
||||
header_offset,
|
||||
.table,
|
||||
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
||||
@as(u32, 1),
|
||||
);
|
||||
}
|
||||
|
||||
// Memory section
|
||||
if (!self.base.options.import_memory) {
|
||||
const header_offset = try reserveVecSectionHeader(file);
|
||||
@ -743,6 +781,31 @@ pub fn flushModule(self: *Wasm, comp: *Compilation) !void {
|
||||
);
|
||||
}
|
||||
|
||||
// element section (function table)
|
||||
if (self.function_table.count() > 0) {
|
||||
const header_offset = try reserveVecSectionHeader(file);
|
||||
const writer = file.writer();
|
||||
|
||||
var flags: u32 = 0x2; // Yes we have a table
|
||||
try leb.writeULEB128(writer, flags);
|
||||
try leb.writeULEB128(writer, @as(u32, 0)); // index of that table. TODO: Store synthetic symbols
|
||||
try emitInit(writer, .{ .i32_const = 0 });
|
||||
try leb.writeULEB128(writer, @as(u8, 0));
|
||||
try leb.writeULEB128(writer, @intCast(u32, self.function_table.count()));
|
||||
var symbol_it = self.function_table.keyIterator();
|
||||
while (symbol_it.next()) |symbol_index_ptr| {
|
||||
try leb.writeULEB128(writer, self.symbols.items[symbol_index_ptr.*].index);
|
||||
}
|
||||
|
||||
try writeVecSectionHeader(
|
||||
file,
|
||||
header_offset,
|
||||
.element,
|
||||
@intCast(u32, (try file.getPos()) - header_offset - header_size),
|
||||
@as(u32, 1),
|
||||
);
|
||||
}
|
||||
|
||||
// Code section
|
||||
if (self.code_section_index) |code_index| {
|
||||
const header_offset = try reserveVecSectionHeader(file);
|
||||
@ -1233,16 +1296,3 @@ pub fn putOrGetFuncType(self: *Wasm, func_type: wasm.Type) !u32 {
|
||||
});
|
||||
return index;
|
||||
}
|
||||
|
||||
/// From a given index and an `ExternalKind`, finds the corresponding Import.
|
||||
/// This is due to indexes for imports being unique per type, rather than across all imports.
|
||||
fn findImport(self: Wasm, index: u32, external_type: wasm.ExternalKind) ?*wasm.Import {
|
||||
var current_index: u32 = 0;
|
||||
for (self.imports.items) |*import| {
|
||||
if (import.kind == external_type) {
|
||||
if (current_index == index) return import;
|
||||
current_index += 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ pub fn resolveRelocs(self: *Atom, wasm_bin: *const Wasm) !void {
|
||||
|
||||
for (self.relocs.items) |reloc| {
|
||||
const value = try relocationValue(reloc, wasm_bin);
|
||||
log.debug("Relocating '{s}' referenced in '{s}' offset=0x{x:0>8} value={d}\n", .{
|
||||
log.debug("Relocating '{s}' referenced in '{s}' offset=0x{x:0>8} value={d}", .{
|
||||
wasm_bin.symbols.items[reloc.index].name,
|
||||
symbol.name,
|
||||
reloc.offset,
|
||||
@ -129,7 +129,7 @@ fn relocationValue(relocation: types.Relocation, wasm_bin: *const Wasm) !u64 {
|
||||
.R_WASM_TABLE_INDEX_I64,
|
||||
.R_WASM_TABLE_INDEX_SLEB,
|
||||
.R_WASM_TABLE_INDEX_SLEB64,
|
||||
=> return error.TodoImplementTableIndex, // find table index from a function symbol
|
||||
=> return wasm_bin.function_table.get(relocation.index) orelse 0,
|
||||
.R_WASM_TYPE_INDEX_LEB => wasm_bin.functions.items[symbol.index].type_index,
|
||||
.R_WASM_GLOBAL_INDEX_I32,
|
||||
.R_WASM_GLOBAL_INDEX_LEB,
|
||||
@ -152,9 +152,7 @@ fn relocationValue(relocation: types.Relocation, wasm_bin: *const Wasm) !u64 {
|
||||
target_atom = target_atom.next orelse break;
|
||||
}
|
||||
const segment = wasm_bin.segments.items[atom_index];
|
||||
const base = wasm_bin.base.options.global_base orelse 1024;
|
||||
const offset = target_atom.offset + segment.offset;
|
||||
break :blk offset + base + (relocation.addend orelse 0);
|
||||
break :blk target_atom.offset + segment.offset + (relocation.addend orelse 0);
|
||||
},
|
||||
.R_WASM_EVENT_INDEX_LEB => symbol.index,
|
||||
.R_WASM_SECTION_OFFSET_I32,
|
||||
|
||||
@ -26,7 +26,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("hello world with updates", linux_x64);
|
||||
|
||||
case.addError("", &[_][]const u8{
|
||||
":97:9: error: struct 'tmp.tmp' has no member named 'main'",
|
||||
":99:9: error: struct 'tmp.tmp' has no member named 'main'",
|
||||
});
|
||||
|
||||
// Incorrect return type
|
||||
|
||||
@ -14,7 +14,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
{
|
||||
var case = ctx.exe("darwin hello world with updates", target);
|
||||
case.addError("", &[_][]const u8{
|
||||
":97:9: error: struct 'tmp.tmp' has no member named 'main'",
|
||||
":99:9: error: struct 'tmp.tmp' has no member named 'main'",
|
||||
});
|
||||
|
||||
// Incorrect return type
|
||||
|
||||
@ -11,7 +11,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm function calls", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ foo();
|
||||
\\ bar();
|
||||
\\ return 42;
|
||||
@ -26,7 +26,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i64 {
|
||||
\\pub fn main() u8 {
|
||||
\\ bar();
|
||||
\\ foo();
|
||||
\\ foo();
|
||||
@ -44,10 +44,10 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() f32 {
|
||||
\\pub fn main() void {
|
||||
\\ bar();
|
||||
\\ foo();
|
||||
\\ return 42.0;
|
||||
\\ return;
|
||||
\\}
|
||||
\\fn foo() void {
|
||||
\\ bar();
|
||||
@ -56,15 +56,15 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
\\}
|
||||
\\fn bar() void {}
|
||||
,
|
||||
"42\n",
|
||||
"0\n",
|
||||
);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ foo(10, 20);
|
||||
\\ return 5;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) void { _ = x; _ = y; }
|
||||
\\fn foo(x: u8, y: u8) void { _ = x; _ = y; }
|
||||
, "5\n");
|
||||
}
|
||||
|
||||
@ -72,10 +72,10 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm locals", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ var y: f32 = 42.0;
|
||||
\\ var x: u32 = 10;
|
||||
\\ var x: u8 = 10;
|
||||
\\ if (false) {
|
||||
\\ y;
|
||||
\\ x;
|
||||
@ -85,18 +85,18 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ var y: f32 = 42.0;
|
||||
\\ _ = y;
|
||||
\\ var x: u32 = 10;
|
||||
\\ var x: u8 = 10;
|
||||
\\ foo(i, x);
|
||||
\\ i = x;
|
||||
\\ return i;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) void {
|
||||
\\fn foo(x: u8, y: u8) void {
|
||||
\\ _ = y;
|
||||
\\ var i: u32 = 10;
|
||||
\\ var i: u8 = 10;
|
||||
\\ i = x;
|
||||
\\}
|
||||
, "10\n");
|
||||
@ -106,228 +106,246 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm binary operands", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i += 20;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "25\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i32 = 2147483647;
|
||||
\\ return i +% 1;
|
||||
\\}
|
||||
, "-2147483648\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\ var i: i4 = 7;
|
||||
\\ return i +% 1;
|
||||
\\ if (i +% 1 != -2147483648) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i4 = 7;
|
||||
\\ if (i +% 1 != 0) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 255;
|
||||
\\ return i +% 1;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i += 20;
|
||||
\\ var result: u32 = foo(i, 10);
|
||||
\\ var result: u8 = foo(i, 10);
|
||||
\\ return result;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) u32 {
|
||||
\\fn foo(x: u8, y: u8) u8 {
|
||||
\\ return x + y;
|
||||
\\}
|
||||
, "35\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 20;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 20;
|
||||
\\ i -= 5;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "15\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i32 = -2147483648;
|
||||
\\ return i -% 1;
|
||||
\\ if (i -% 1 != 2147483647) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "2147483647\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i7 = -64;
|
||||
\\ return i -% 1;
|
||||
\\ if (i -% 1 != 63) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "63\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u4 = 0;
|
||||
\\ return i -% 1;
|
||||
\\}
|
||||
, "15\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i -= 3;
|
||||
\\ var result: u32 = foo(i, 10);
|
||||
\\ var result: u8 = foo(i, 10);
|
||||
\\ return result;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) u32 {
|
||||
\\fn foo(x: u8, y: u8) u8 {
|
||||
\\ return y - x;
|
||||
\\}
|
||||
, "8\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: u32 = 5;
|
||||
\\ i *= 7;
|
||||
\\ var result: u32 = foo(i, 10);
|
||||
\\ return result;
|
||||
\\ if (result != 350) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) u32 {
|
||||
\\ return x * y;
|
||||
\\}
|
||||
, "350\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i32 = 2147483647;
|
||||
\\ return i *% 2;
|
||||
\\ const result = i *% 2;
|
||||
\\ if (result != -2) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "-2\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: u3 = 3;
|
||||
\\ return i *% 3;
|
||||
\\ if (i *% 3 != 1) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: i4 = 3;
|
||||
\\ return i *% 3;
|
||||
\\ if (i *% 3 != 1) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() void {
|
||||
\\ var i: u32 = 352;
|
||||
\\ i /= 7; // i = 50
|
||||
\\ var result: u32 = foo(i, 7);
|
||||
\\ return result;
|
||||
\\ if (result != 7) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
\\fn foo(x: u32, y: u32) u32 {
|
||||
\\ return x / y;
|
||||
\\}
|
||||
, "7\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i &= 6;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "4\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i |= 6;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "7\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ i ^= 6;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "3\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = false;
|
||||
\\ b = b or false;
|
||||
\\ return b;
|
||||
\\ if (b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = true;
|
||||
\\ b = b or false;
|
||||
\\ return b;
|
||||
\\ if (!b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = false;
|
||||
\\ b = b or true;
|
||||
\\ return b;
|
||||
\\ if (!b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = true;
|
||||
\\ b = b or true;
|
||||
\\ return b;
|
||||
\\ if (!b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = false;
|
||||
\\ b = b and false;
|
||||
\\ return b;
|
||||
\\ if (b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = true;
|
||||
\\ b = b and false;
|
||||
\\ return b;
|
||||
\\ if (b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = false;
|
||||
\\ b = b and true;
|
||||
\\ return b;
|
||||
\\ if (b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() bool {
|
||||
\\pub fn main() void {
|
||||
\\ var b: bool = true;
|
||||
\\ b = b and true;
|
||||
\\ return b;
|
||||
\\ if (!b) unreachable;
|
||||
\\ return;
|
||||
\\}
|
||||
, "1\n");
|
||||
, "0\n");
|
||||
}
|
||||
|
||||
{
|
||||
var case = ctx.exe("wasm conditions", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\ if (i > @as(u32, 4)) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ if (i > @as(u8, 4)) {
|
||||
\\ i += 10;
|
||||
\\ }
|
||||
\\ return i;
|
||||
@ -335,9 +353,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "15\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\ if (i < @as(u32, 4)) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ if (i < @as(u8, 4)) {
|
||||
\\ i += 10;
|
||||
\\ } else {
|
||||
\\ i = 2;
|
||||
@ -347,11 +365,11 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "2\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 5;
|
||||
\\ if (i < @as(u32, 4)) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 5;
|
||||
\\ if (i < @as(u8, 4)) {
|
||||
\\ i += 10;
|
||||
\\ } else if(i == @as(u32, 5)) {
|
||||
\\ } else if(i == @as(u8, 5)) {
|
||||
\\ i = 20;
|
||||
\\ }
|
||||
\\ return i;
|
||||
@ -359,12 +377,12 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "20\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 11;
|
||||
\\ if (i < @as(u32, 4)) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 11;
|
||||
\\ if (i < @as(u8, 4)) {
|
||||
\\ i += 10;
|
||||
\\ } else {
|
||||
\\ if (i > @as(u32, 10)) {
|
||||
\\ if (i > @as(u8, 10)) {
|
||||
\\ i += 20;
|
||||
\\ } else {
|
||||
\\ i = 20;
|
||||
@ -375,7 +393,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "31\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() void {
|
||||
\\pub fn main() void {
|
||||
\\ assert(foo(true) != @as(i32, 30));
|
||||
\\}
|
||||
\\
|
||||
@ -387,10 +405,10 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
\\ const x = if(ok) @as(i32, 20) else @as(i32, 10);
|
||||
\\ return x;
|
||||
\\}
|
||||
, "");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() void {
|
||||
\\pub fn main() void {
|
||||
\\ assert(foo(false) == @as(i32, 20));
|
||||
\\ assert(foo(true) == @as(i32, 30));
|
||||
\\}
|
||||
@ -407,16 +425,16 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
\\ };
|
||||
\\ return val + 10;
|
||||
\\}
|
||||
, "");
|
||||
, "0\n");
|
||||
}
|
||||
|
||||
{
|
||||
var case = ctx.exe("wasm while loops", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 0;
|
||||
\\ while(i < @as(u32, 5)){
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 0;
|
||||
\\ while(i < @as(u8, 5)){
|
||||
\\ i += 1;
|
||||
\\ }
|
||||
\\
|
||||
@ -425,10 +443,10 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 0;
|
||||
\\ while(i < @as(u32, 10)){
|
||||
\\ var x: u32 = 1;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 0;
|
||||
\\ while(i < @as(u8, 10)){
|
||||
\\ var x: u8 = 1;
|
||||
\\ i += x;
|
||||
\\ }
|
||||
\\ return i;
|
||||
@ -436,12 +454,12 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "10\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var i: u32 = 0;
|
||||
\\ while(i < @as(u32, 10)){
|
||||
\\ var x: u32 = 1;
|
||||
\\pub fn main() u8 {
|
||||
\\ var i: u8 = 0;
|
||||
\\ while(i < @as(u8, 10)){
|
||||
\\ var x: u8 = 1;
|
||||
\\ i += x;
|
||||
\\ if (i == @as(u32, 5)) break;
|
||||
\\ if (i == @as(u8, 5)) break;
|
||||
\\ }
|
||||
\\ return i;
|
||||
\\}
|
||||
@ -454,7 +472,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
case.addCompareOutput(
|
||||
\\const Number = enum { One, Two, Three };
|
||||
\\
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var number1 = Number.One;
|
||||
\\ var number2: Number = .Two;
|
||||
\\ if (false) {
|
||||
@ -462,47 +480,52 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
\\ number2;
|
||||
\\ }
|
||||
\\ const number3 = @intToEnum(Number, 2);
|
||||
\\
|
||||
\\ return @enumToInt(number3);
|
||||
\\ if (@enumToInt(number3) != 2) {
|
||||
\\ unreachable;
|
||||
\\ }
|
||||
\\ return;
|
||||
\\}
|
||||
, "2\n");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Number = enum { One, Two, Three };
|
||||
\\
|
||||
\\pub export fn _start() i32 {
|
||||
\\pub fn main() void {
|
||||
\\ var number1 = Number.One;
|
||||
\\ var number2: Number = .Two;
|
||||
\\ const number3 = @intToEnum(Number, 2);
|
||||
\\ if (number1 == number2) return 1;
|
||||
\\ if (number2 == number3) return 1;
|
||||
\\ if (@enumToInt(number1) != 0) return 1;
|
||||
\\ if (@enumToInt(number2) != 1) return 1;
|
||||
\\ if (@enumToInt(number3) != 2) return 1;
|
||||
\\ assert(number1 != number2);
|
||||
\\ assert(number2 != number3);
|
||||
\\ assert(@enumToInt(number1) == 0);
|
||||
\\ assert(@enumToInt(number2) == 1);
|
||||
\\ assert(@enumToInt(number3) == 2);
|
||||
\\ var x: Number = .Two;
|
||||
\\ if (number2 != x) return 1;
|
||||
\\ assert(number2 == x);
|
||||
\\
|
||||
\\ return @enumToInt(number3);
|
||||
\\ return;
|
||||
\\}
|
||||
, "2\n");
|
||||
\\fn assert(val: bool) void {
|
||||
\\ if(!val) unreachable;
|
||||
\\}
|
||||
, "0\n");
|
||||
}
|
||||
|
||||
{
|
||||
var case = ctx.exe("wasm structs", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Example = struct { x: u32 };
|
||||
\\const Example = struct { x: u8 };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var example: Example = .{ .x = 5 };
|
||||
\\ return example.x;
|
||||
\\}
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Example = struct { x: u32 };
|
||||
\\const Example = struct { x: u8 };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var example: Example = .{ .x = 5 };
|
||||
\\ example.x = 10;
|
||||
\\ return example.x;
|
||||
@ -510,18 +533,18 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "10\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Example = struct { x: u32, y: u32 };
|
||||
\\const Example = struct { x: u8, y: u8 };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var example: Example = .{ .x = 5, .y = 10 };
|
||||
\\ return example.y + example.x;
|
||||
\\}
|
||||
, "15\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Example = struct { x: u32, y: u32 };
|
||||
\\const Example = struct { x: u8, y: u8 };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var example: Example = .{ .x = 5, .y = 10 };
|
||||
\\ var example2: Example = .{ .x = 10, .y = 20 };
|
||||
\\
|
||||
@ -531,9 +554,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "30\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\const Example = struct { x: u32, y: u32 };
|
||||
\\const Example = struct { x: u8, y: u8 };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var example: Example = .{ .x = 5, .y = 10 };
|
||||
\\
|
||||
\\ example = .{ .x = 10, .y = 20 };
|
||||
@ -546,9 +569,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm switch", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var val: u32 = 1;
|
||||
\\ var a: u32 = switch (val) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var val: u8 = 1;
|
||||
\\ var a: u8 = switch (val) {
|
||||
\\ 0, 1 => 2,
|
||||
\\ 2 => 3,
|
||||
\\ 3 => 4,
|
||||
@ -560,9 +583,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "2\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var val: u32 = 2;
|
||||
\\ var a: u32 = switch (val) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var val: u8 = 2;
|
||||
\\ var a: u8 = switch (val) {
|
||||
\\ 0, 1 => 2,
|
||||
\\ 2 => 3,
|
||||
\\ 3 => 4,
|
||||
@ -574,9 +597,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "3\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var val: u32 = 10;
|
||||
\\ var a: u32 = switch (val) {
|
||||
\\pub fn main() u8 {
|
||||
\\ var val: u8 = 10;
|
||||
\\ var a: u8 = switch (val) {
|
||||
\\ 0, 1 => 2,
|
||||
\\ 2 => 3,
|
||||
\\ 3 => 4,
|
||||
@ -590,9 +613,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
case.addCompareOutput(
|
||||
\\const MyEnum = enum { One, Two, Three };
|
||||
\\
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var val: MyEnum = .Two;
|
||||
\\ var a: u32 = switch (val) {
|
||||
\\ var a: u8 = switch (val) {
|
||||
\\ .One => 1,
|
||||
\\ .Two => 2,
|
||||
\\ .Three => 3,
|
||||
@ -607,7 +630,7 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm error unions", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() void {
|
||||
\\pub fn main() void {
|
||||
\\ var e1 = error.Foo;
|
||||
\\ var e2 = error.Bar;
|
||||
\\ assert(e1 != e2);
|
||||
@ -618,32 +641,32 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
\\fn assert(b: bool) void {
|
||||
\\ if (!b) unreachable;
|
||||
\\}
|
||||
, "");
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var e: anyerror!u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var e: anyerror!u8 = 5;
|
||||
\\ const i = e catch 10;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var e: anyerror!u32 = error.Foo;
|
||||
\\pub fn main() u8 {
|
||||
\\ var e: anyerror!u8 = error.Foo;
|
||||
\\ const i = e catch 10;
|
||||
\\ return i;
|
||||
\\}
|
||||
, "10\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var e = foo();
|
||||
\\ const i = e catch 69;
|
||||
\\ return i;
|
||||
\\}
|
||||
\\
|
||||
\\fn foo() anyerror!u32 {
|
||||
\\fn foo() anyerror!u8 {
|
||||
\\ return 5;
|
||||
\\}
|
||||
, "5\n");
|
||||
@ -653,24 +676,24 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm error union part 2", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var e = foo();
|
||||
\\ const i = e catch 69;
|
||||
\\ return i;
|
||||
\\}
|
||||
\\
|
||||
\\fn foo() anyerror!u32 {
|
||||
\\fn foo() anyerror!u8 {
|
||||
\\ return error.Bruh;
|
||||
\\}
|
||||
, "69\n");
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\pub fn main() u8 {
|
||||
\\ var e = foo();
|
||||
\\ const i = e catch 42;
|
||||
\\ return i;
|
||||
\\}
|
||||
\\
|
||||
\\fn foo() anyerror!u32 {
|
||||
\\fn foo() anyerror!u8 {
|
||||
\\ return error.Dab;
|
||||
\\}
|
||||
, "42\n");
|
||||
@ -680,20 +703,22 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm integer widening", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u64 {
|
||||
\\ var x: u32 = 5;
|
||||
\\ return x;
|
||||
\\pub fn main() void{
|
||||
\\ var x: u8 = 5;
|
||||
\\ var y: u64 = x;
|
||||
\\ _ = y;
|
||||
\\ return;
|
||||
\\}
|
||||
, "5\n");
|
||||
, "0\n");
|
||||
}
|
||||
|
||||
{
|
||||
var case = ctx.exe("wasm optionals", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: ?u32 = 5;
|
||||
\\ var y: u32 = 0;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: ?u8 = 5;
|
||||
\\ var y: u8 = 0;
|
||||
\\ if (x) |val| {
|
||||
\\ y = val;
|
||||
\\ }
|
||||
@ -702,9 +727,9 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: ?u32 = null;
|
||||
\\ var y: u32 = 0;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: ?u8 = null;
|
||||
\\ var y: u8 = 0;
|
||||
\\ if (x) |val| {
|
||||
\\ y = val;
|
||||
\\ }
|
||||
@ -713,23 +738,23 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
, "0\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: ?u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: ?u8 = 5;
|
||||
\\ return x.?;
|
||||
\\}
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: u32 = 5;
|
||||
\\ var y: ?u32 = x;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: u8 = 5;
|
||||
\\ var y: ?u8 = x;
|
||||
\\ return y.?;
|
||||
\\}
|
||||
, "5\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var val: ?u32 = 5;
|
||||
\\pub fn main() u8 {
|
||||
\\ var val: ?u8 = 5;
|
||||
\\ while (val) |*v| {
|
||||
\\ v.* -= 1;
|
||||
\\ if (v.* == 2) {
|
||||
@ -745,32 +770,32 @@ pub fn addCases(ctx: *TestContext) !void {
|
||||
var case = ctx.exe("wasm pointers", wasi);
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: u32 = 0;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: u8 = 0;
|
||||
\\
|
||||
\\ foo(&x);
|
||||
\\ return x;
|
||||
\\}
|
||||
\\
|
||||
\\fn foo(x: *u32)void {
|
||||
\\fn foo(x: *u8)void {
|
||||
\\ x.* = 2;
|
||||
\\}
|
||||
, "2\n");
|
||||
|
||||
case.addCompareOutput(
|
||||
\\pub export fn _start() u32 {
|
||||
\\ var x: u32 = 0;
|
||||
\\pub fn main() u8 {
|
||||
\\ var x: u8 = 0;
|
||||
\\
|
||||
\\ foo(&x);
|
||||
\\ bar(&x);
|
||||
\\ return x;
|
||||
\\}
|
||||
\\
|
||||
\\fn foo(x: *u32)void {
|
||||
\\fn foo(x: *u8)void {
|
||||
\\ x.* = 2;
|
||||
\\}
|
||||
\\
|
||||
\\fn bar(x: *u32) void {
|
||||
\\fn bar(x: *u8) void {
|
||||
\\ x.* += 2;
|
||||
\\}
|
||||
, "4\n");
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user