diff --git a/toolchain/check/cpp/export.cpp b/toolchain/check/cpp/export.cpp index a5e2bea73213..192a36d54b62 100644 --- a/toolchain/check/cpp/export.cpp +++ b/toolchain/check/cpp/export.cpp @@ -1254,9 +1254,24 @@ static auto BuildCppToCarbonThunkBody(Context& context, stmts.push_back(call.get()); if (has_return_value) { - auto* return_stmt = clang::ReturnStmt::Create( - sema.getASTContext(), clang_loc, return_storage_expr.get(), - return_storage_var_decl); + // Return `return_storage` by value. The variable is an NRVO candidate, + // so CodeGen constructs it directly in the return slot and ignores the + // returned expression, but the AST should still model a by-value return + // (a prvalue) rather than returning an lvalue referring to the local. + clang::QualType return_type = return_storage_var_decl->getType(); + clang::Expr* return_val_expr = sema.BuildDeclRefExpr( + return_storage_var_decl, return_type, clang::VK_LValue, clang_loc); + // TODO: Lvalue-to-rvalue conversion isn't valid for class types. Those + // would need a (elided) copy/move construction, which may not exist. + if (!return_type->getAsCXXRecordDecl() && !return_type->isNullPtrType()) { + return_val_expr = clang::ImplicitCastExpr::Create( + sema.getASTContext(), return_type, clang::CK_LValueToRValue, + return_val_expr, /*BasePath=*/nullptr, clang::VK_PRValue, + clang::FPOptionsOverride()); + } + auto* return_stmt = + clang::ReturnStmt::Create(sema.getASTContext(), clang_loc, + return_val_expr, return_storage_var_decl); stmts.push_back(return_stmt); } } diff --git a/toolchain/check/testdata/interop/cpp/function/export/thunk_ast.carbon b/toolchain/check/testdata/interop/cpp/function/export/thunk_ast.carbon index 0e77bb335775..9ed7ad38d3ff 100644 --- a/toolchain/check/testdata/interop/cpp/function/export/thunk_ast.carbon +++ b/toolchain/check/testdata/interop/cpp/function/export/thunk_ast.carbon @@ -35,20 +35,21 @@ fn F(i: i32) -> i32 { // CHECK:STDOUT: | | | `-ImplicitCastExpr {{0x[a-f0-9]+}} 'const int' lvalue // CHECK:STDOUT: | | | `-DeclRefExpr {{0x[a-f0-9]+}} 'int' lvalue Var {{0x[a-f0-9]+}} 'return_storage' 'int' // CHECK:STDOUT: | | `-ReturnStmt {{0x[a-f0-9]+}} nrvo_candidate(Var {{0x[a-f0-9]+}} 'return_storage' 'int') -// CHECK:STDOUT: | | `-DeclRefExpr {{0x[a-f0-9]+}} 'int' lvalue Var {{0x[a-f0-9]+}} 'return_storage' 'int' +// CHECK:STDOUT: | | `-ImplicitCastExpr {{0x[a-f0-9]+}} 'int' +// CHECK:STDOUT: | | `-DeclRefExpr {{0x[a-f0-9]+}} 'int' lvalue Var {{0x[a-f0-9]+}} 'return_storage' 'int' // CHECK:STDOUT: | |-AlwaysInlineAttr {{0x[a-f0-9]+}} <> Implicit always_inline // CHECK:STDOUT: | `-InternalLinkageAttr {{0x[a-f0-9]+}} <> Implicit -// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} line:38:5 in thunk_with_args_and_return.carbon hidden G 'int (int)' external-linkage +// CHECK:STDOUT: |-FunctionDecl {{0x[a-f0-9]+}} line:39:5 in thunk_with_args_and_return.carbon hidden G 'int (int)' external-linkage // CHECK:STDOUT: | |-ParmVarDecl {{0x[a-f0-9]+}} col:11 in thunk_with_args_and_return.carbon hidden used i 'int' -// CHECK:STDOUT: | `-CompoundStmt {{0x[a-f0-9]+}} -// CHECK:STDOUT: | `-ReturnStmt {{0x[a-f0-9]+}} +// CHECK:STDOUT: | `-CompoundStmt {{0x[a-f0-9]+}} +// CHECK:STDOUT: | `-ReturnStmt {{0x[a-f0-9]+}} // CHECK:STDOUT: | `-CallExpr {{0x[a-f0-9]+}} 'int' // CHECK:STDOUT: | |-ImplicitCastExpr {{0x[a-f0-9]+}} 'int (*)(int)' // CHECK:STDOUT: | | `-DeclRefExpr {{0x[a-f0-9]+}} 'int (int)' lvalue Function {{0x[a-f0-9]+}} 'F' 'int (int)' // CHECK:STDOUT: | | `-NestedNameSpecifier Namespace {{0x[a-f0-9]+}} 'Carbon' // CHECK:STDOUT: | `-ImplicitCastExpr {{0x[a-f0-9]+}} 'int' // CHECK:STDOUT: | `-DeclRefExpr {{0x[a-f0-9]+}} 'int' lvalue ParmVar {{0x[a-f0-9]+}} 'i' 'int' -// CHECK:STDOUT: `-ImportDecl {{0x[a-f0-9]+}} col:4 in thunk_with_args_and_return.carbon implicit thunk_with_args_and_return.carbon +// CHECK:STDOUT: `-ImportDecl {{0x[a-f0-9]+}} col:4 in thunk_with_args_and_return.carbon implicit thunk_with_args_and_return.carbon return i; }