diff --git a/c2rust-transpile/src/translator/builtins.rs b/c2rust-transpile/src/translator/builtins.rs index ded87d2ce3..1ee2ec74cd 100644 --- a/c2rust-transpile/src/translator/builtins.rs +++ b/c2rust-transpile/src/translator/builtins.rs @@ -680,32 +680,102 @@ impl<'c> Translation<'c> { method_name: &str, args: &[CExprId], ) -> TranslationResult>> { + let &[a_arg, b_arg, out_arg] = args else { + return Err(TranslationError::generic( + "`convert_overflow_arith` must have exactly 3 arguments", + )); + }; + + let arg_type = |arg: CExprId| { + self.ast_context + .index_unwrap_parens(arg) + .kind + .get_type() + .ok_or_else(|| TranslationError::generic("overflow builtin argument has no type")) + }; + let result_ty_id = self + .ast_context + .get_pointee_qual_type(arg_type(out_arg)?) + .ok_or_else(|| TranslationError::generic("overflow builtin output is not a pointer"))? + .ctype; + let type_kind = |ty| &self.ast_context.resolve_type(ty).kind; + let a_kind = type_kind(arg_type(a_arg)?); + let b_kind = type_kind(arg_type(b_arg)?); + let result_kind = type_kind(result_ty_id); + + // The `s`/`u`-prefixed builtins use one common type; the type-generic + // ones accept operand and result types that all differ. With a common + // type, Rust's native `overflowing_*` matches the builtin exactly. + // Otherwise, overflow is defined against the infinite-precision result, + // so compute in `i128` and check that narrowing to the result type + // preserves the value. + let same_types = a_kind == b_kind && b_kind == result_kind; + let is_128_bit = |kind: &CTypeKind| matches!(kind, CTypeKind::Int128 | CTypeKind::UInt128); + if !same_types && [a_kind, b_kind, result_kind].into_iter().any(is_128_bit) { + // The `i128` computation below cannot represent all values of + // `unsigned __int128` operands or results. Reject rather than + // translate wrongly: https://github.com/immunant/c2rust/issues/1878 + return Err(TranslationError::generic( + "mixed-type overflow builtins involving 128-bit integers are not supported", + )); + } + let result_ty = if same_types { + None + } else { + Some(self.convert_type(result_ty_id)?) + }; + let args = self.convert_exprs(ctx.used(), args, None)?; args.and_then_try(|args| { - let [a, b, c]: [_; 3] = args - .try_into() - .map_err(|_| "`convert_overflow_arith` must have exactly 3 arguments")?; + let [a, b, out]: [_; 3] = args.try_into().map_err(|_| "expected 3 arguments")?; + let (a, b) = if same_types { + (a, b) + } else { + (cast_int(a, "i128", true), cast_int(b, "i128", true)) + }; let overflowing = mk().method_call_expr(a, method_name, vec![b]); - let sum_name = self.renamer.borrow_mut().pick_name("c2rust_result"); + let result_name = self.renamer.borrow_mut().pick_name("c2rust_result"); let over_name = self.renamer.borrow_mut().pick_name("c2rust_overflowed"); - let overflow_let = mk().local_stmt(Box::new(mk().local( + let mut stmts = vec![mk().local_stmt(Box::new(mk().local( mk().tuple_pat(vec![ - mk().ident_pat(&sum_name), - mk().ident_pat(over_name.clone()), + mk().ident_pat(&result_name), + mk().ident_pat(&over_name), ]), None, Some(overflowing), + )))]; + let mut overflowed = mk().ident_expr(&over_name); + let mut out_name = result_name.clone(); + + if let Some(result_ty) = result_ty { + // `c2rust_result as result_ty` truncates silently. Casting back + // up and comparing against `c2rust_result` detects when that + // truncation lost information -- overflow distinct from + // `overflowing`'s own flag, which only fires when even `i128` + // cannot hold the result. + let narrow_name = self.renamer.borrow_mut().pick_name("c2rust_result_narrow"); + stmts.push(mk().local_stmt(Box::new(mk().local( + mk().ident_pat(&narrow_name), + None, + Some(mk().cast_expr(mk().ident_expr(&result_name), result_ty)), + )))); + let roundtrip = + mk().cast_expr(mk().ident_expr(&narrow_name), mk().path_ty(vec!["i128"])); + let truncated = mk().binary_expr( + BinOp::Ne(Default::default()), + roundtrip, + mk().ident_expr(&result_name), + ); + overflowed = mk().binary_expr(BinOp::Or(Default::default()), overflowed, truncated); + out_name = narrow_name; + } + + stmts.push(mk().expr_stmt(mk().assign_expr( + mk().unary_expr(UnOp::Deref(Default::default()), out), + mk().ident_expr(&out_name), ))); - let out_assign = mk().assign_expr( - mk().unary_expr(UnOp::Deref(Default::default()), c), - mk().ident_expr(&sum_name), - ); - - Ok(WithStmts::new( - vec![overflow_let, mk().expr_stmt(out_assign)], - mk().ident_expr(over_name), - )) + Ok(WithStmts::new(stmts, overflowed)) }) } diff --git a/c2rust-transpile/tests/snapshots.rs b/c2rust-transpile/tests/snapshots.rs index 68c042a62a..5b72ce1293 100644 --- a/c2rust-transpile/tests/snapshots.rs +++ b/c2rust-transpile/tests/snapshots.rs @@ -104,6 +104,7 @@ fn transpile_snapshot( c_path: &Path, edition: RustEdition, expect_compile_error: bool, + expect_translation_error: bool, imported_crates: &[&str], ) { let c_file_name = c_path.file_name().unwrap().to_str().unwrap(); @@ -126,7 +127,10 @@ fn transpile_snapshot( (0.., _) => "clang15", }; - let cfg = config(edition); + let mut cfg = config(edition); + if expect_translation_error { + cfg.fail_on_error = false; + } compile_and_transpile_file(c_path, cfg); let cwd = current_dir().unwrap(); // The crate name can't have `.`s in it, so use the file stem. @@ -171,6 +175,7 @@ struct TranspileTest<'a> { os_specific: bool, expect_compile_error_edition_2021: bool, expect_compile_error_edition_2024: bool, + expect_translation_error: bool, imported_crates: Vec<&'a str>, } @@ -181,6 +186,7 @@ fn transpile(c_file_name: &str) -> TranspileTest { os_specific: false, expect_compile_error_edition_2021: false, expect_compile_error_edition_2024: false, + expect_translation_error: false, imported_crates: Default::default(), } } @@ -226,6 +232,15 @@ impl<'a> TranspileTest<'a> { .expect_compile_error_edition_2024(expect_error) } + /// Expect some decls to fail translation. Failed decls are omitted from + /// the output instead of aborting, so the snapshot pins what is rejected. + pub fn expect_translation_error(self, expect_translation_error: bool) -> Self { + Self { + expect_translation_error, + ..self + } + } + pub fn expect_unresolved_import(mut self, imported_crate: &'a str) -> Self { self.imported_crates.push(imported_crate); self @@ -238,6 +253,7 @@ impl<'a> TranspileTest<'a> { os_specific, expect_compile_error_edition_2021, expect_compile_error_edition_2024, + expect_translation_error, imported_crates, } = self; @@ -289,6 +305,7 @@ impl<'a> TranspileTest<'a> { &c_path, Edition2021, expect_compile_error_edition_2021, + expect_translation_error, &imported_crates, ); transpile_snapshot( @@ -296,6 +313,7 @@ impl<'a> TranspileTest<'a> { &c_path, Edition2024, expect_compile_error_edition_2024, + expect_translation_error, &imported_crates, ); } @@ -442,6 +460,13 @@ fn test_out_of_range_lit() { transpile("out_of_range_lit.c").run(); } +#[test] +fn test_overflow_128() { + transpile("overflow_128.c") + .expect_translation_error(true) + .run(); +} + #[test] fn test_predefined() { transpile("predefined.c").run(); diff --git a/c2rust-transpile/tests/snapshots/overflow_128.c b/c2rust-transpile/tests/snapshots/overflow_128.c new file mode 100644 index 0000000000..c69ec370ff --- /dev/null +++ b/c2rust-transpile/tests/snapshots/overflow_128.c @@ -0,0 +1,30 @@ +// Overflow builtins mixing 128-bit and other integer types are rejected, so +// the `reject`-prefixed functions must be missing from the snapshot. +// See https://github.com/immunant/c2rust/issues/1878. + +int reject_uint128_operands(unsigned __int128 a, unsigned __int128 b, + unsigned long long *out) +{ + return __builtin_add_overflow(a, b, out); +} + +int reject_uint128_result(int a, int b, unsigned __int128 *out) +{ + return __builtin_mul_overflow(a, b, out); +} + +int reject_int128_operand(__int128 a, long long b, long long *out) +{ + return __builtin_sub_overflow(a, b, out); +} + +int supported_uint128(unsigned __int128 a, unsigned __int128 b, + unsigned __int128 *out) +{ + return __builtin_add_overflow(a, b, out); +} + +int supported_int128(__int128 a, __int128 b, __int128 *out) +{ + return __builtin_mul_overflow(a, b, out); +} diff --git a/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2021.clang15.snap b/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2021.clang15.snap new file mode 100644 index 0000000000..d9551ae2e2 --- /dev/null +++ b/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2021.clang15.snap @@ -0,0 +1,33 @@ +--- +source: c2rust-transpile/tests/snapshots.rs +expression: cat tests/snapshots/overflow_128.2021.clang15.rs +--- +#![allow( + clippy::missing_safety_doc, + dead_code, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + unused_assignments, + unused_mut +)] +#[no_mangle] +pub unsafe extern "C" fn supported_uint128( + mut a: u128, + mut b: u128, + mut out: *mut u128, +) -> ::core::ffi::c_int { + let (c2rust_result, c2rust_overflowed) = a.overflowing_add(b); + *out = c2rust_result; + return c2rust_overflowed as ::core::ffi::c_int; +} +#[no_mangle] +pub unsafe extern "C" fn supported_int128( + mut a: i128, + mut b: i128, + mut out: *mut i128, +) -> ::core::ffi::c_int { + let (c2rust_result, c2rust_overflowed) = a.overflowing_mul(b); + *out = c2rust_result; + return c2rust_overflowed as ::core::ffi::c_int; +} diff --git a/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2024.clang15.snap b/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2024.clang15.snap new file mode 100644 index 0000000000..0686a8b433 --- /dev/null +++ b/c2rust-transpile/tests/snapshots/snapshots__transpile@overflow_128.c.2024.clang15.snap @@ -0,0 +1,34 @@ +--- +source: c2rust-transpile/tests/snapshots.rs +expression: cat tests/snapshots/overflow_128.2024.clang15.rs +--- +#![allow( + clippy::missing_safety_doc, + dead_code, + non_camel_case_types, + non_snake_case, + non_upper_case_globals, + unsafe_op_in_unsafe_fn, + unused_assignments, + unused_mut +)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn supported_uint128( + mut a: u128, + mut b: u128, + mut out: *mut u128, +) -> ::core::ffi::c_int { + let (c2rust_result, c2rust_overflowed) = a.overflowing_add(b); + *out = c2rust_result; + return c2rust_overflowed as ::core::ffi::c_int; +} +#[unsafe(no_mangle)] +pub unsafe extern "C" fn supported_int128( + mut a: i128, + mut b: i128, + mut out: *mut i128, +) -> ::core::ffi::c_int { + let (c2rust_result, c2rust_overflowed) = a.overflowing_mul(b); + *out = c2rust_result; + return c2rust_overflowed as ::core::ffi::c_int; +} diff --git a/tests/unit/builtins/src/overflow.c b/tests/unit/builtins/src/overflow.c new file mode 100644 index 0000000000..7539140afa --- /dev/null +++ b/tests/unit/builtins/src/overflow.c @@ -0,0 +1,138 @@ +#include +#include + +// Operands of distinct same-width types (size_t and unsigned long translate +// to the distinct Rust types usize and c_ulong even on LP64). +void overflow_builtins(const unsigned buffer_size, int buffer[const]) +{ + int i = 0; + size_t sz = 1000; + unsigned long ul = 7; + size_t out_sz = 0; + unsigned long out_ul = 0; + + // One common type, so these map to Rust's native `overflowing_*` methods. + buffer[i++] = __builtin_add_overflow(sz, out_sz, &out_sz); + buffer[i++] = (int)out_sz; + buffer[i++] = __builtin_mul_overflow(ul, ul, &out_ul); + buffer[i++] = (int)out_ul; + + buffer[i++] = __builtin_mul_overflow(sz, ul, &out_sz); + buffer[i++] = (int)out_sz; + buffer[i++] = __builtin_add_overflow(sz, ul, &out_sz); + buffer[i++] = (int)out_sz; + buffer[i++] = __builtin_sub_overflow(ul, sz, &out_ul); + buffer[i++] = (int)out_ul; + + // Actually overflowing. + buffer[i++] = __builtin_mul_overflow((size_t)-1, (unsigned long)2, &out_sz); + buffer[i++] = (int)out_sz; + buffer[i++] = __builtin_mul_overflow((size_t)-1, (unsigned long)-1, &out_sz); + buffer[i++] = (int)out_sz; +} + +// The `s`/`u`-prefixed variants fix one common type, so they also map to +// Rust's native `overflowing_*` methods. +void overflow_builtins_prefixed(const unsigned buffer_size, int buffer[const]) +{ + int i = 0; + unsigned out_u = 0; + unsigned long long out_ull = 0; + int out_i = 0; + long out_l = 0; + + buffer[i++] = __builtin_uadd_overflow(UINT_MAX, 1u, &out_u); + buffer[i++] = (int)out_u; + buffer[i++] = __builtin_uaddll_overflow(ULLONG_MAX, 2ULL, &out_ull); + buffer[i++] = (int)out_ull; + buffer[i++] = __builtin_usub_overflow(0u, 1u, &out_u); + buffer[i++] = (int)out_u; + buffer[i++] = __builtin_sadd_overflow(INT_MAX, 1, &out_i); + buffer[i++] = out_i; + buffer[i++] = __builtin_smull_overflow(LONG_MAX, 2L, &out_l); + buffer[i++] = (int)out_l; + buffer[i++] = __builtin_smul_overflow(3, 5, &out_i); + buffer[i++] = out_i; +} + +// Same-type 128-bit uses also map to Rust's native `overflowing_*` methods +// (mixed-type uses involving 128-bit integers are rejected instead). +void overflow_builtins_uint128(const unsigned buffer_size, int buffer[const]) +{ + int i = 0; + __uint128_t max = ~(__uint128_t)0; + __uint128_t one = 1; + __uint128_t three = 3; + __uint128_t four = 4; + __uint128_t out = 0; + + buffer[i++] = __builtin_add_overflow(max, one, &out); + buffer[i++] = (int)(out >> 64); + buffer[i++] = (int)out; + + // (2^128 - 1) is divisible by 3, so this fits exactly. + buffer[i++] = __builtin_mul_overflow(max / three, three, &out); + buffer[i++] = (int)(out >> 64); + buffer[i++] = (int)out; + buffer[i++] = __builtin_mul_overflow(max / three, four, &out); + buffer[i++] = (int)(out >> 64); + buffer[i++] = (int)out; + + buffer[i++] = __builtin_sub_overflow(one, max, &out); + buffer[i++] = (int)(out >> 64); + buffer[i++] = (int)out; +} + +// Result type narrower than the operands. +void overflow_builtins_narrow_result(const unsigned buffer_size, int buffer[const]) +{ + int i = 0; + long long ll1 = 5000000000LL; + long long ll2 = 5000000000LL; + int narrow_out = 0; + buffer[i++] = __builtin_add_overflow(ll1, ll2, &narrow_out); + buffer[i++] = narrow_out; + + long long ll3 = -5000000000LL; + long long ll4 = 3LL; + buffer[i++] = __builtin_sub_overflow(ll3, ll4, &narrow_out); + buffer[i++] = narrow_out; + + long long ll5 = 5000000000LL; + long long ll6 = 2LL; + buffer[i++] = __builtin_mul_overflow(ll5, ll6, &narrow_out); + buffer[i++] = narrow_out; +} + +// Operands of mixed signedness and width. +void overflow_builtins_mixed(const unsigned buffer_size, int buffer[const]) +{ + int i = 0; + size_t out_sz = 0; + long long out_ll = 0; + + // -1 + 0 is not representable in size_t. + int negative = -1; + size_t sz = 0; + buffer[i++] = __builtin_add_overflow(negative, sz, &out_sz); + buffer[i++] = (int)(out_sz >> 32); + buffer[i++] = (int)out_sz; + + // 7 + 1000 is. + int seven = 7; + sz = 1000; + buffer[i++] = __builtin_add_overflow(seven, sz, &out_sz); + buffer[i++] = (int)out_sz; + + // Product of distinct operand types overflows even 128-bit precision. + unsigned long long ull_max = ~0ULL; + buffer[i++] = __builtin_mul_overflow(ull_max, (size_t)-1, &out_sz); + buffer[i++] = (int)out_sz; + + // Unsigned operands, signed result. + unsigned above_int_max = 3000000000u; + unsigned long nine = 9; + buffer[i++] = __builtin_add_overflow(above_int_max, nine, &out_ll); + buffer[i++] = (int)(out_ll >> 32); + buffer[i++] = (int)out_ll; +} diff --git a/tests/unit/builtins/src/test_builtins.rs b/tests/unit/builtins/src/test_builtins.rs index c29fed3da6..cb3490a72a 100644 --- a/tests/unit/builtins/src/test_builtins.rs +++ b/tests/unit/builtins/src/test_builtins.rs @@ -4,6 +4,10 @@ use crate::alloca::rust_alloca_hello; use crate::atomics::{rust_atomics_entry, rust_fences, rust_new_atomics}; use crate::math::{rust_ffs, rust_ffsl, rust_ffsll, rust_isfinite, rust_isinf_sign, rust_isnan}; use crate::mem_x_fns::{rust_assume_aligned, rust_mem_x}; +use crate::overflow::{ + rust_overflow_builtins, rust_overflow_builtins_mixed, rust_overflow_builtins_narrow_result, + rust_overflow_builtins_prefixed, rust_overflow_builtins_uint128, +}; use std::ffi::{c_char, c_double, c_int, c_long, c_longlong, c_uint}; #[link(name = "test")] @@ -19,6 +23,11 @@ unsafe extern "C" { fn isfinite(_: c_double) -> c_int; fn isnan(_: c_double) -> c_int; fn isinf_sign(_: c_double) -> c_int; + fn overflow_builtins(_: c_uint, _: *mut c_int); + fn overflow_builtins_prefixed(_: c_uint, _: *mut c_int); + fn overflow_builtins_uint128(_: c_uint, _: *mut c_int); + fn overflow_builtins_narrow_result(_: c_uint, _: *mut c_int); + fn overflow_builtins_mixed(_: c_uint, _: *mut c_int); } const BUFFER_SIZE: usize = 1024; @@ -78,6 +87,52 @@ pub fn test_fences() { } } +/// Run the C and Rust versions of a buffer-filling function and compare the buffers. +fn compare_c_and_rust( + c_fn: unsafe extern "C" fn(c_uint, *mut c_int), + rust_fn: unsafe extern "C" fn(c_uint, *mut c_int), +) { + let mut buffer = [0; BUFFER_SIZE]; + let mut rust_buffer = [0; BUFFER_SIZE]; + + unsafe { + c_fn(BUFFER_SIZE as u32, buffer.as_mut_ptr()); + rust_fn(BUFFER_SIZE as u32, rust_buffer.as_mut_ptr()); + } + + for index in 0..BUFFER_SIZE { + assert_eq!(buffer[index], rust_buffer[index]); + } +} + +#[test] +pub fn test_overflow_builtins() { + compare_c_and_rust(overflow_builtins, rust_overflow_builtins); +} + +#[test] +pub fn test_overflow_builtins_prefixed() { + compare_c_and_rust(overflow_builtins_prefixed, rust_overflow_builtins_prefixed); +} + +#[test] +pub fn test_overflow_builtins_uint128() { + compare_c_and_rust(overflow_builtins_uint128, rust_overflow_builtins_uint128); +} + +#[test] +pub fn test_overflow_builtins_narrow_result() { + compare_c_and_rust( + overflow_builtins_narrow_result, + rust_overflow_builtins_narrow_result, + ); +} + +#[test] +pub fn test_overflow_builtins_mixed() { + compare_c_and_rust(overflow_builtins_mixed, rust_overflow_builtins_mixed); +} + #[test] pub fn test_mem_fns() { let const_string = "I am ten!\0";