From fb0028a0d7b43a2a5dd05f075ded22746f92faf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20=27vesim=27=20Kuli=C5=84ski?= Date: Sat, 7 Sep 2024 17:29:43 +0200 Subject: [PATCH] mips: fix C ABI compatibility --- src/arch/mips/abi.zig | 86 +++++++++++++++++++++++++++++++++++++++++++ src/codegen/llvm.zig | 22 +++++++++-- test/c_abi/cfuncs.c | 14 +++---- test/c_abi/main.zig | 66 ++++++++++++++++----------------- 4 files changed, 145 insertions(+), 43 deletions(-) create mode 100644 src/arch/mips/abi.zig diff --git a/src/arch/mips/abi.zig b/src/arch/mips/abi.zig new file mode 100644 index 0000000000..8f3cebd21c --- /dev/null +++ b/src/arch/mips/abi.zig @@ -0,0 +1,86 @@ +const std = @import("std"); +const Type = @import("../../Type.zig"); +const Zcu = @import("../../Zcu.zig"); +const assert = std.debug.assert; + +pub const Class = union(enum) { + memory, + byval, + i32_array: u8, +}; + +pub const Context = enum { ret, arg }; + +pub fn classifyType(ty: Type, zcu: *Zcu, ctx: Context) Class { + const target = zcu.getTarget(); + std.debug.assert(ty.hasRuntimeBitsIgnoreComptime(zcu)); + + const max_direct_size = target.ptrBitWidth() * 2; + switch (ty.zigTypeTag(zcu)) { + .@"struct" => { + const bit_size = ty.bitSize(zcu); + if (ty.containerLayout(zcu) == .@"packed") { + if (bit_size > max_direct_size) return .memory; + return .byval; + } + if (bit_size > max_direct_size) return .memory; + // TODO: for bit_size <= 32 using byval is more correct, but that needs inreg argument attribute + const count = @as(u8, @intCast(std.mem.alignForward(u64, bit_size, 32) / 32)); + return .{ .i32_array = count }; + }, + .@"union" => { + const bit_size = ty.bitSize(zcu); + if (ty.containerLayout(zcu) == .@"packed") { + if (bit_size > max_direct_size) return .memory; + return .byval; + } + if (bit_size > max_direct_size) return .memory; + + return .byval; + }, + .bool => return .byval, + .float => return .byval, + .int, .@"enum", .error_set => { + const bit_size = ty.bitSize(zcu); + if (bit_size > max_direct_size) return .memory; + return .byval; + }, + .vector => { + const elem_type = ty.elemType2(zcu); + switch (elem_type.zigTypeTag(zcu)) { + .bool, .int => { + const bit_size = ty.bitSize(zcu); + if (ctx == .ret and bit_size > 128) return .memory; + if (bit_size > 512) return .memory; + // TODO: byval vector arguments with non power of 2 size need inreg attribute + return .byval; + }, + .float => return .memory, + else => unreachable, + } + }, + .optional => { + std.debug.assert(ty.isPtrLikeOptional(zcu)); + return .byval; + }, + .pointer => { + std.debug.assert(!ty.isSlice(zcu)); + return .byval; + }, + .error_union, + .frame, + .@"anyframe", + .noreturn, + .void, + .type, + .comptime_float, + .comptime_int, + .undefined, + .null, + .@"fn", + .@"opaque", + .enum_literal, + .array, + => unreachable, + } +} diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index dbaaa92597..2f9f9e096b 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -26,6 +26,7 @@ const wasm_c_abi = @import("../arch/wasm/abi.zig"); const aarch64_c_abi = @import("../arch/aarch64/abi.zig"); const arm_c_abi = @import("../arch/arm/abi.zig"); const riscv_c_abi = @import("../arch/riscv64/abi.zig"); +const mips_c_abi = @import("../arch/mips/abi.zig"); const dev = @import("../dev.zig"); const target_util = @import("../target.zig"); @@ -11681,7 +11682,10 @@ fn firstParamSRet(fn_info: InternPool.Key.FuncType, zcu: *Zcu, target: std.Targe return switch (fn_info.cc) { .Unspecified, .Inline => returnTypeByRef(zcu, target, return_type), .C => switch (target.cpu.arch) { - .mips, .mipsel => false, + .mips, .mipsel => switch (mips_c_abi.classifyType(return_type, zcu, .ret)) { + .memory, .i32_array => true, + .byval => false, + }, .x86 => isByRef(return_type, zcu), .x86_64 => switch (target.os.tag) { .windows => x86_64_abi.classifyWindows(return_type, zcu) == .memory, @@ -11732,7 +11736,12 @@ fn lowerFnRetTy(o: *Object, fn_info: InternPool.Key.FuncType) Allocator.Error!Bu .C => { switch (target.cpu.arch) { - .mips, .mipsel => return o.lowerType(return_type), + .mips, .mipsel => { + switch (mips_c_abi.classifyType(return_type, zcu, .ret)) { + .memory, .i32_array => return .void, + .byval => return o.lowerType(return_type), + } + }, .x86 => return if (isByRef(return_type, zcu)) .void else o.lowerType(return_type), .x86_64 => switch (target.os.tag) { .windows => return lowerWin64FnRetTy(o, fn_info), @@ -11978,7 +11987,14 @@ const ParamTypeIterator = struct { .mips, .mipsel => { it.zig_index += 1; it.llvm_index += 1; - return .byval; + switch (mips_c_abi.classifyType(ty, zcu, .arg)) { + .memory => { + it.byval_attr = true; + return .byref; + }, + .byval => return .byval, + .i32_array => |size| return Lowering{ .i32_array = size }, + } }, .x86_64 => switch (target.os.tag) { .windows => return it.nextWin64(ty), diff --git a/test/c_abi/cfuncs.c b/test/c_abi/cfuncs.c index 028f6e06d2..92f95c339c 100644 --- a/test/c_abi/cfuncs.c +++ b/test/c_abi/cfuncs.c @@ -2657,7 +2657,7 @@ void run_c_tests(void) { } #endif -#if !defined(__mips__) && !defined(ZIG_PPC32) +#if !defined(ZIG_PPC32) { struct Struct_u64_u64 s = zig_ret_struct_u64_u64(); assert_or_panic(s.a == 1); @@ -2708,7 +2708,7 @@ void run_c_tests(void) { #endif #if !defined __i386__ && !defined __arm__ && !defined __aarch64__ && \ - !defined __mips__ && !defined __powerpc__ && !defined ZIG_RISCV64 + !defined __powerpc__ && !defined ZIG_RISCV64 { struct SmallStructInts s = {1, 2, 3, 4}; zig_small_struct_ints(s); @@ -2716,7 +2716,7 @@ void run_c_tests(void) { #endif #if !defined __arm__ && !defined __aarch64__ && \ - !defined __mips__ && !defined __powerpc__ && !defined ZIG_RISCV64 + !defined __powerpc__ && !defined ZIG_RISCV64 { struct MedStructInts s = {1, 2, 3}; zig_med_struct_ints(s); @@ -2741,7 +2741,7 @@ void run_c_tests(void) { zig_small_packed_struct(s); } -#if !defined __i386__ && !defined __arm__ && !defined __mips__ && \ +#if !defined __i386__ && !defined __arm__ && \ !defined ZIG_PPC32 && !defined _ARCH_PPC64 { struct SplitStructInts s = {1234, 100, 1337}; @@ -2756,7 +2756,7 @@ void run_c_tests(void) { } #endif -#if !defined __i386__ && !defined __arm__ && !defined __mips__ && \ +#if !defined __i386__ && !defined __arm__ && \ !defined ZIG_PPC32 && !defined _ARCH_PPC64 { struct SplitStructMixed s = {1234, 100, 1337.0f}; @@ -2764,7 +2764,7 @@ void run_c_tests(void) { } #endif -#if !defined __mips__ && !defined ZIG_PPC32 +#if !defined ZIG_PPC32 { struct BigStruct s = {30, 31, 32, 33, 34}; struct BigStruct res = zig_big_struct_both(s); @@ -2784,7 +2784,7 @@ void run_c_tests(void) { } #endif -#if !defined __mips__ && !defined ZIG_PPC32 +#if !defined ZIG_PPC32 { struct FloatRect r1 = {1, 21, 16, 4}; struct FloatRect r2 = {178, 189, 21, 15}; diff --git a/test/c_abi/main.zig b/test/c_abi/main.zig index 86b9584a6e..09b5011665 100644 --- a/test/c_abi/main.zig +++ b/test/c_abi/main.zig @@ -322,7 +322,7 @@ extern fn c_struct_u64_u64_7(usize, usize, usize, usize, usize, usize, usize, St extern fn c_struct_u64_u64_8(usize, usize, usize, usize, usize, usize, usize, usize, Struct_u64_u64) void; test "C ABI struct u64 u64" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const s = c_ret_struct_u64_u64(); @@ -359,7 +359,7 @@ extern fn c_ret_struct_f32f32_f32() Struct_f32f32_f32; extern fn c_struct_f32f32_f32(Struct_f32f32_f32) void; test "C ABI struct {f32,f32} f32" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const s = c_ret_struct_f32f32_f32(); @@ -389,7 +389,7 @@ extern fn c_ret_struct_f32_f32f32() Struct_f32_f32f32; extern fn c_struct_f32_f32f32(Struct_f32_f32f32) void; test "C ABI struct f32 {f32,f32}" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const s = c_ret_struct_f32_f32f32(); @@ -424,7 +424,7 @@ extern fn c_ret_struct_u32_union_u32_u32u32() Struct_u32_Union_u32_u32u32; extern fn c_struct_u32_union_u32_u32u32(Struct_u32_Union_u32_u32u32) void; test "C ABI struct{u32,union{u32,struct{u32,u32}}}" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = c_ret_struct_u32_union_u32_u32u32(); @@ -444,7 +444,7 @@ const BigStruct = extern struct { extern fn c_big_struct(BigStruct) void; test "C ABI big struct" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const s = BigStruct{ @@ -503,7 +503,7 @@ extern fn c_med_struct_mixed(MedStructMixed) void; extern fn c_ret_med_struct_mixed() MedStructMixed; test "C ABI medium struct of ints and floats" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = MedStructMixed{ @@ -535,7 +535,7 @@ extern fn c_ret_small_struct_ints() SmallStructInts; test "C ABI small struct of ints" { if (builtin.cpu.arch == .x86) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = SmallStructInts{ @@ -568,7 +568,7 @@ extern fn c_med_struct_ints(MedStructInts) void; extern fn c_ret_med_struct_ints() MedStructInts; test "C ABI medium struct of ints" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = MedStructInts{ @@ -646,7 +646,7 @@ extern fn c_split_struct_ints(SplitStructInt) void; test "C ABI split struct of ints" { if (builtin.cpu.arch == .x86) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = SplitStructInt{ @@ -673,7 +673,7 @@ extern fn c_ret_split_struct_mixed() SplitStructMixed; test "C ABI split struct of ints and floats" { if (builtin.cpu.arch == .x86) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const s = SplitStructMixed{ @@ -700,7 +700,7 @@ extern fn c_multiple_struct_ints(Rect, Rect) void; extern fn c_multiple_struct_floats(FloatRect, FloatRect) void; test "C ABI sret and byval together" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const s = BigStruct{ @@ -752,7 +752,7 @@ const Vector5 = extern struct { extern fn c_big_struct_floats(Vector5) void; test "C ABI structs of floats as parameter" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const v3 = Vector3{ @@ -828,7 +828,7 @@ export fn zig_multiple_struct_floats(x: FloatRect, y: FloatRect) void { } test "C ABI structs of floats as multiple parameters" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; const r1 = FloatRect{ @@ -941,7 +941,7 @@ extern fn c_ret_struct_with_array() StructWithArray; test "Struct with array as padding." { if (builtin.cpu.arch == .x86) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; c_struct_with_array(.{ .a = 1, .padding = undefined, .b = 2 }); @@ -966,7 +966,7 @@ extern fn c_float_array_struct(FloatArrayStruct) void; extern fn c_ret_float_array_struct() FloatArrayStruct; test "Float array like struct" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; c_float_array_struct(.{ @@ -1031,7 +1031,7 @@ extern fn c_ret_big_vec() BigVec; test "big simd vector" { if (builtin.zig_backend == .stage2_x86_64) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC64()) return error.SkipZigTest; if (builtin.zig_backend == .stage2_llvm and builtin.cpu.arch == .x86_64 and builtin.os.tag == .macos and builtin.mode != .Debug) return error.SkipZigTest; @@ -5340,7 +5340,7 @@ extern fn c_ptr_size_float_struct(Vector2) void; extern fn c_ret_ptr_size_float_struct() Vector2; test "C ABI pointer sized float struct" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; @@ -5362,25 +5362,25 @@ pub inline fn expectOk(c_err: c_int) !void { /// Tests for Double + Char struct const DC = extern struct { v1: f64, v2: u8 }; test "DC: Zig passes to C" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_DC(.{ .v1 = -0.25, .v2 = 15 })); } test "DC: Zig returns to C" { - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_ret_DC()); } test "DC: C passes to Zig" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_send_DC()); } test "DC: C returns to Zig" { - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectEqual(DC{ .v1 = -0.25, .v2 = 15 }, c_ret_DC()); @@ -5406,12 +5406,12 @@ const CFF = extern struct { v1: u8, v2: f32, v3: f32 }; test "CFF: Zig passes to C" { if (builtin.target.cpu.arch == .x86) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_CFF(.{ .v1 = 39, .v2 = 0.875, .v3 = 1.0 })); } test "CFF: Zig returns to C" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_ret_CFF()); } @@ -5419,7 +5419,7 @@ test "CFF: C passes to Zig" { if (builtin.target.cpu.arch == .x86) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch == .aarch64 and builtin.mode != .Debug) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_send_CFF()); @@ -5427,7 +5427,7 @@ test "CFF: C passes to Zig" { test "CFF: C returns to Zig" { if (builtin.cpu.arch == .aarch64 and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isRISCV() and builtin.mode != .Debug) return error.SkipZigTest; - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectEqual(CFF{ .v1 = 39, .v2 = 0.875, .v3 = 1.0 }, c_ret_CFF()); } @@ -5451,22 +5451,22 @@ pub export fn zig_ret_CFF() CFF { const PD = extern struct { v1: ?*anyopaque, v2: f64 }; test "PD: Zig passes to C" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_PD(.{ .v1 = null, .v2 = 0.5 })); } test "PD: Zig returns to C" { - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_assert_ret_PD()); } test "PD: C passes to Zig" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectOk(c_send_PD()); } test "PD: C returns to Zig" { - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; try expectEqual(PD{ .v1 = null, .v2 = 0.5 }, c_ret_PD()); } @@ -5520,7 +5520,7 @@ const ByVal = extern struct { extern fn c_func_ptr_byval(*anyopaque, *anyopaque, ByVal, c_ulong, *anyopaque, c_ulong) void; test "C function that takes byval struct called via function pointer" { - if (builtin.cpu.arch.isMIPS() and builtin.mode != .Debug) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64() and builtin.mode != .Debug) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; var fn_ptr = &c_func_ptr_byval; @@ -5551,7 +5551,7 @@ const f16_struct = extern struct { }; extern fn c_f16_struct(f16_struct) f16_struct; test "f16 struct" { - if (builtin.target.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.target.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.target.cpu.arch.isPowerPC32()) return error.SkipZigTest; if (builtin.cpu.arch.isARM() and builtin.mode != .Debug) return error.SkipZigTest; @@ -5666,7 +5666,7 @@ const Coord2 = extern struct { extern fn stdcall_coord2(Coord2, Coord2, Coord2) callconv(stdcall_callconv) Coord2; test "Stdcall ABI structs" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC()) return error.SkipZigTest; const res = stdcall_coord2( @@ -5751,7 +5751,7 @@ const byval_tail_callsite_attr = struct { }; test "byval tail callsite attribute" { - if (builtin.cpu.arch.isMIPS()) return error.SkipZigTest; + if (builtin.cpu.arch.isMIPS64()) return error.SkipZigTest; if (builtin.cpu.arch.isPowerPC32()) return error.SkipZigTest; // Originally reported at https://github.com/ziglang/zig/issues/16290