From 72445cf667709cbcf753a943558f7de45944828c Mon Sep 17 00:00:00 2001 From: Frank Date: Fri, 7 Aug 2026 23:48:24 -0400 Subject: [PATCH] fix fixed-array borrow lowering across modules --- transpiler/src/codegen/emit_expr.rs | 3 ++ transpiler/src/codegen/emit_stmt.rs | 37 ++++++++++++++ transpiler/src/codegen/mod.rs | 37 ++++++++++++++ transpiler/tests/e2e_basic.rs | 76 +++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+) diff --git a/transpiler/src/codegen/emit_expr.rs b/transpiler/src/codegen/emit_expr.rs index 81cad205..bdd3df57 100644 --- a/transpiler/src/codegen/emit_expr.rs +++ b/transpiler/src/codegen/emit_expr.rs @@ -20863,6 +20863,9 @@ impl CodeGen { .iter() .enumerate() .map(|(idx, arg)| { + if let Some(array_lvalue) = self.try_emit_fixed_array_local_borrow_call_arg(arg) { + return array_lvalue; + } let declared_arg_expected_ty = self.lookup_function_arg_expected_type(call.func.as_ref(), idx); if self diff --git a/transpiler/src/codegen/emit_stmt.rs b/transpiler/src/codegen/emit_stmt.rs index b3257773..72f0fa59 100644 --- a/transpiler/src/codegen/emit_stmt.rs +++ b/transpiler/src/codegen/emit_stmt.rs @@ -3020,6 +3020,12 @@ impl CodeGen { pub(super) fn emit_local(&mut self, local: &syn::Local) { let pat = &local.pat; self.register_local_binding_pattern(pat); + // Preserve repeat-expression emission as `rusty::array_repeat(...)`, + // but retain its unambiguous fixed-array type for later call-argument + // lowering. This must be recorded after emitting the initializer: using + // it as the initializer's expected type selects the typed std::array + // materialization path instead. + let deferred_fixed_array_repeat_ty = self.fixed_array_repeat_local_type(local); // `let PAT = EXPR else { DIVERGE };` — the diverge block was silently // DROPPED (the None path aborted through an unguarded unwrap) and the // matched-path bindings dangled (auto&& over an unwrap() prvalue). @@ -5678,6 +5684,37 @@ impl CodeGen { } } } + if let Some(ty) = deferred_fixed_array_repeat_ty { + self.update_local_binding_type_for_pattern(pat, ty); + } + } + + /// Infer the source-level fixed array type of a simple local initialized by + /// `[seed; len]`. This is deliberately narrower than general initializer + /// inference: it exists so a later `&local` / `&mut local` can preserve the + /// C++ `std::array` lvalue ABI without changing repeat-expression emission. + fn fixed_array_repeat_local_type(&self, local: &syn::Local) -> Option { + let syn::Pat::Ident(pat_ident) = &local.pat else { + return None; + }; + if pat_ident.subpat.is_some() { + return None; + } + let init = local.init.as_ref()?; + let syn::Expr::Repeat(repeat) = self.peel_paren_group_expr(&init.expr) else { + return None; + }; + let elem_ty = self + .infer_simple_expr_type(&repeat.expr) + .or_else(|| self.infer_local_binding_type_from_initializer(&repeat.expr))?; + let len = &repeat.len; + syn::parse2::(quote::quote!([#elem_ty; #len])).ok() + } + + fn update_local_binding_type_for_pattern(&mut self, pat: &syn::Pat, ty: syn::Type) { + if let syn::Pat::Ident(pat_ident) = pat { + self.update_local_binding_type(pat_ident.ident.to_string(), ty); + } } pub(super) fn emit_if_expr_to_string( diff --git a/transpiler/src/codegen/mod.rs b/transpiler/src/codegen/mod.rs index dd366ba6..c64643cb 100644 --- a/transpiler/src/codegen/mod.rs +++ b/transpiler/src/codegen/mod.rs @@ -13171,6 +13171,40 @@ impl CodeGen { } } + /// C++ represents Rust fixed arrays as `std::array`, whose references bind + /// directly to the array lvalue. Do not route a borrow of a local fixed + /// array through the generic pointer/span lowering: that loses compatibility + /// with cross-module `&[T; N]` parameters, which retain their + /// `std::array&` ABI. + /// + /// Restrict this to stable *local* paths. Fields, indexes, raw pointers, + /// literals, and other rvalues retain their existing borrow lowering. + fn try_emit_fixed_array_local_borrow_call_arg(&self, arg: &syn::Expr) -> Option { + let syn::Expr::Reference(reference) = self.peel_paren_group_expr(arg) else { + return None; + }; + if !self.is_stable_reference_lvalue_expr(&reference.expr) { + return None; + } + let syn::Expr::Path(path) = self.peel_paren_group_expr(&reference.expr) else { + return None; + }; + if path.qself.is_some() || path.path.segments.len() != 1 { + return None; + } + let local_name = path.path.segments[0].ident.to_string(); + let local_ty = self + .lookup_local_binding_type(&local_name) + .or_else(|| self.infer_simple_expr_type(&reference.expr)); + if !local_ty + .as_ref() + .is_some_and(|ty| self.type_is_fixed_array_like(ty)) + { + return None; + } + Some(self.emit_expr_to_string_with_expected(&reference.expr, None)) + } + fn emit_deserializer_call_arg(&self, expr: &syn::Expr) -> String { if let syn::Expr::Reference(reference) = self.peel_paren_group_expr(expr) && reference.mutability.is_some() @@ -13225,6 +13259,9 @@ impl CodeGen { if let Some(wrapped) = self.try_emit_interface_traits_dyn_ref_coercion(arg, expected_ty) { return wrapped; } + if let Some(array_lvalue) = self.try_emit_fixed_array_local_borrow_call_arg(arg) { + return array_lvalue; + } let arg_is_closure = matches!(self.peel_paren_group_expr(arg), syn::Expr::Closure(_)); let suppress_placeholder_expected_for_closure = arg_is_closure diff --git a/transpiler/tests/e2e_basic.rs b/transpiler/tests/e2e_basic.rs index aabb5893..e95d3ff4 100644 --- a/transpiler/tests/e2e_basic.rs +++ b/transpiler/tests/e2e_basic.rs @@ -371,6 +371,82 @@ fn test_crate_mode_basic() { assert!(stdout.contains("2 files transpiled")); } +// Regression fixture for srpc.wire's cross-module fixed-array borrow ABI. +// +// A cross-module callee exposes `&[u8; N]` as `const std::array<...>&`; both +// shared and mutable borrows of an array local must therefore lower to the bare +// lvalue. The same-module case does not cover this because its callers are tests. +#[test] +fn test_crate_mode_cross_module_fixed_array_borrow_abi() { + let dir = tempfile::tempdir().unwrap(); + let src_dir = dir.path().join("src"); + std::fs::create_dir(&src_dir).unwrap(); + + std::fs::write( + dir.path().join("Cargo.toml"), + "[package]\nname = \"srpc_wire_array_borrow\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n[lib]\nname = \"srpc_wire_array_borrow\"\n", + ) + .unwrap(); + std::fs::write(src_dir.join("lib.rs"), "pub mod varint;\npub mod serde;\n").unwrap(); + std::fs::write( + src_dir.join("varint.rs"), + "pub fn load32(buf: &[u8; 9]) -> i32 { buf[0] as i32 }\n\ + pub fn dump32(val: i32, buf: &mut [u8; 9]) -> usize { buf[0] = val as u8; 1 }\n", + ) + .unwrap(); + std::fs::write( + src_dir.join("serde.rs"), + "use super::varint;\n\ + pub fn ser(v: i32) -> usize {\n\ + let mut b = [0u8; 9];\n\ + let n = varint::dump32(v, &mut b);\n\ + let _ = varint::load32(&b);\n\ + n\n\ + }\n", + ) + .unwrap(); + + let out_dir = dir.path().join("cpp_out"); + let output = transpiler_bin() + .arg("--crate") + .arg(dir.path().join("Cargo.toml").to_str().unwrap()) + .arg("--output-dir") + .arg(&out_dir) + .output() + .expect("failed to transpile crate fixture"); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + + let varint_cpp = std::fs::read_to_string(out_dir.join("srpc_wire_array_borrow.varint.cppm")) + .unwrap(); + let serde_cpp = std::fs::read_to_string(out_dir.join("srpc_wire_array_borrow.serde.cppm")) + .unwrap(); + + assert!( + varint_cpp.contains("const std::array& buf"), + "unexpected varint ABI:\n{varint_cpp}" + ); + assert!( + varint_cpp.contains("std::array& buf"), + "unexpected varint ABI:\n{varint_cpp}" + ); + assert!( + serde_cpp.contains("varint::dump32(") && serde_cpp.contains(", b)"), + "mutable array borrow must pass its bare lvalue:\n{serde_cpp}" + ); + assert!( + serde_cpp.contains("varint::load32(b)"), + "shared array borrow must pass its bare lvalue:\n{serde_cpp}" + ); + assert!( + !serde_cpp.contains("rusty::as_slice(b)"), + "array borrow must not become a span when the callee ABI is std::array&:\n{serde_cpp}" + ); +} + #[test] fn test_crate_mode_missing_cargo_toml() { let dir = tempfile::tempdir().unwrap();