Repository: immunant/c2rust (2025-08-13T06:59:31Z)

Issue:
transpile: const macros cause type errors as they aren't translated with portable types
The combination of #1266 and #1305 caused an issue with how const macro translation and portable types are implemented.

With #1266 adding support for portable types, `override_ty`s were threaded through `fn convert_expr`'s call tree containing the overriding portable type.

However, in translating const macros, we started from the expansion exprs to recreate the const macro.  This lacked the parent `fn convert_expr` tree that piped down the correct `override_ty`, so we end up translating const macros with a non-portable type, and then when the const macro is used, we get type errors.

Getting the correct `override_ty` to the const macros is tricky because the const macros can be translated before their uses, so we haven't yet determined the correct `override_ty`.  Likely, we want to separate the portable type/`override_ty` determination into a separate stage of the transpiler that runs before all `fn convert_*` functions, but as that's a bigger change and I want to make sure I'm doing the right thing and not missing something, I'll go for a simpler solution first: just adding casts at all const macro use sites to the correct portable type, as we do have the `override_ty` available when translating the const macro expansions, just not the const macro itself.

Alternatively, we could take an approach like https://github.com/immunant/c2rust/issues/1295#issuecomment-3140258043 and translate const macros as plain (sometimes `const`) generic `fn`s.  This is more similar to how C macros work, with the expansion happening at use site, instead of having to have a singular definition that works for all uses.  Using a generic here would allow the type to be determined at the use/call site, which is simpler to do as described above.


Discussion:

Patch:
diff --git a/c2rust-transpile/src/lib.rs b/c2rust-transpile/src/lib.rs
index e7609f68d1..7c2f2ff0d6 100644
--- a/c2rust-transpile/src/lib.rs
+++ b/c2rust-transpile/src/lib.rs
@@ -46,10 +46,10 @@ type TranspileResult = Result<(PathBuf, PragmaVec, CrateSet), ()>;
 #[derive(Default, Debug)]
 pub enum TranslateMacros {
     /// Don't translate any macros.
-    #[default]
     None,
 
     /// Translate the conservative subset of macros known to always work.
+    #[default]
     Conservative,
 
     /// Try to translate more, but this is experimental and not guaranteed to work.
diff --git a/c2rust-transpile/src/translator/mod.rs b/c2rust-transpile/src/translator/mod.rs
index 57e2a53948..b30ce71408 100644
--- a/c2rust-transpile/src/translator/mod.rs
+++ b/c2rust-transpile/src/translator/mod.rs
@@ -3260,7 +3260,7 @@ impl<'c> Translation<'c> {
             self.ast_context[expr_id]
         );
 
-        if let Some(converted) = self.convert_const_macro_expansion(ctx, expr_id)? {
+        if let Some(converted) = self.convert_const_macro_expansion(ctx, expr_id, override_ty)? {
             return Ok(converted);
         }
 
@@ -4095,6 +4095,7 @@ impl<'c> Translation<'c> {
         &self,
         ctx: ExprContext,
         expr_id: CExprId,
+        override_ty: Option<CQualTypeId>,
     ) -> TranslationResult<Option<WithStmts<Box<Expr>>>> {
         let macros = match self.ast_context.macro_invocations.get(&expr_id) {
             Some(macros) => macros.as_slice(),
@@ -4145,7 +4146,13 @@ impl<'c> Translation<'c> {
         let val = WithStmts::new_val(mk().path_expr(vec![rust_name]));
 
         let expr_kind = &self.ast_context[expr_id].kind;
-        if let Some(expr_ty) = expr_kind.get_qual_type() {
+        // TODO We'd like to get rid of this cast eventually (see #1321).
+        // Currently, const macros do not get the correct `override_ty` themselves,
+        // so they aren't declared with the correct portable type,
+        // but its uses are expecting the correct portable type,
+        // so we need to cast it to the `override_ty` here.
+        let expr_ty = override_ty.or_else(|| expr_kind.get_qual_type());
+        if let Some(expr_ty) = expr_ty {
             self.convert_cast(
                 ctx,
                 CQualTypeId::new(macro_ty),
diff --git a/c2rust/src/bin/c2rust-transpile.rs b/c2rust/src/bin/c2rust-transpile.rs
index 4930f98f8b..890664447b 100644
--- a/c2rust/src/bin/c2rust-transpile.rs
+++ b/c2rust/src/bin/c2rust-transpile.rs
@@ -162,10 +162,10 @@ struct Args {
 #[derive(Default, Debug, PartialEq, Eq, ValueEnum, Clone)]
 pub enum TranslateMacros {
     /// Don't translate any macros.
-    #[default]
     None,
 
     /// Translate the conservative subset of macros known to always work.
+    #[default]
     Conservative,
 
     /// Try to translate more, but this is experimental and not guaranteed to work.


Test patch:
diff --git a/c2rust-transpile/tests/snapshots.rs b/c2rust-transpile/tests/snapshots.rs
index 8d42f9f4b1..77ab9afc63 100644
--- a/c2rust-transpile/tests/snapshots.rs
+++ b/c2rust-transpile/tests/snapshots.rs
@@ -3,7 +3,7 @@ use std::fs;
 use std::path::Path;
 use std::process::Command;
 
-use c2rust_transpile::{ReplaceMode, TranslateMacros, TranspilerConfig};
+use c2rust_transpile::{ReplaceMode, TranspilerConfig};
 
 fn config() -> TranspilerConfig {
     TranspilerConfig {
@@ -36,8 +36,8 @@ fn config() -> TranspilerConfig {
         enabled_warnings: Default::default(),
         emit_no_std: false,
         output_dir: None,
-        translate_const_macros: TranslateMacros::Conservative,
-        translate_fn_macros: TranslateMacros::Conservative,
+        translate_const_macros: Default::default(),
+        translate_fn_macros: Default::default(),
         disable_refactoring: false,
         preserve_unused_functions: false,
         log_level: log::LevelFilter::Warn,
diff --git a/c2rust-transpile/tests/snapshots/macros.c b/c2rust-transpile/tests/snapshots/macros.c
index 78d0f110e0..a139aba27e 100644
--- a/c2rust-transpile/tests/snapshots/macros.c
+++ b/c2rust-transpile/tests/snapshots/macros.c
@@ -1,5 +1,6 @@
 #include <stdbool.h>
 #include <stddef.h>
+#include <stdint.h>
 
 struct S {
   int i;
@@ -364,3 +365,7 @@ int local_fn(void) { return 1234; }
 #define LOCAL_VALUE (local_fn())
 
 int use_local_value(void) { return LOCAL_VALUE; }
+
+bool use_portable_type(uintptr_t len) {
+  return len <= UINTPTR_MAX / 2;
+}
diff --git a/c2rust-transpile/tests/snapshots/snapshots__transpile@macros.c.snap b/c2rust-transpile/tests/snapshots/snapshots__transpile@macros.c.snap
index a730e0892b..d5988f37b9 100644
--- a/c2rust-transpile/tests/snapshots/snapshots__transpile@macros.c.snap
+++ b/c2rust-transpile/tests/snapshots/snapshots__transpile@macros.c.snap
@@ -15,6 +15,7 @@ input_file: c2rust-transpile/tests/snapshots/macros.c
 extern "C" {
     fn extern_fn() -> std::ffi::c_int;
 }
+pub type uintptr_t = usize;
 #[derive(Copy, Clone)]
 #[repr(C)]
 pub struct S {
@@ -29,6 +30,7 @@ pub struct fn_ptrs {
     pub fn2: Option<unsafe extern "C" fn(std::ffi::c_int) -> std::ffi::c_int>,
 }
 pub type zstd_platform_dependent_type = std::ffi::c_long;
+pub const UINTPTR_MAX: std::ffi::c_ulong = 18446744073709551615 as std::ffi::c_ulong;
 pub const NESTED_INT: std::ffi::c_int = 0xffff as std::ffi::c_int;
 #[no_mangle]
 pub unsafe extern "C" fn local_muts() {
@@ -446,6 +448,10 @@ pub unsafe extern "C" fn local_fn() -> std::ffi::c_int {
 pub unsafe extern "C" fn use_local_value() -> std::ffi::c_int {
     return local_fn();
 }
+#[no_mangle]
+pub unsafe extern "C" fn use_portable_type(mut len: uintptr_t) -> bool {
+    return len <= (UINTPTR_MAX as uintptr_t).wrapping_div(2 as uintptr_t);
+}
 unsafe extern "C" fn run_static_initializers() {
     global_static_const_ptr_arithmetic = (b"hello\0" as *const u8 as *const std::ffi::c_char)
         .offset(5 as std::ffi::c_int as isize)