summary refs log tree commit diff
diff options
context:
space:
mode:
authorbors <bors@rust-lang.org>2025-05-03 03:52:37 +0000
committerbors <bors@rust-lang.org>2025-05-03 03:52:37 +0000
commit973ec11f61ba77123aafbb2799dd24f291250a2b (patch)
tree92c4f6ba0fce794421e07b35089c2161ed401bec
parent9d6fe3d1b2e24fad0574ef47915df47c72ba2ee3 (diff)
parent260fcc67f395d6961b8c1776c6c5cf64e8839517 (diff)
downloadrust-973ec11f61ba77123aafbb2799dd24f291250a2b.tar.gz
rust-973ec11f61ba77123aafbb2799dd24f291250a2b.zip
Auto merge of #140592 - cuviper:beta-next, r=cuviper
[beta] backports

- Don't allow flattened format_args in const. #139624
- set subsections_via_symbols for ld64 helper sections #139752
- Fix detection of `main` function if there are expressions around it #140220
- rustdoc: Fix doctest heuristic for main fn wrapping #140420
- extend the list of registered dylibs on `test::prepare_cargo_test` #140563

r? cuviper
-rw-r--r--compiler/rustc_codegen_ssa/src/back/link.rs6
-rw-r--r--library/core/src/fmt/rt.rs9
-rw-r--r--src/bootstrap/src/core/build_steps/test.rs6
-rw-r--r--src/librustdoc/doctest/make.rs68
-rw-r--r--tests/rustdoc-ui/doctest/auxiliary/items.rs1
-rw-r--r--tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs4
-rw-r--r--tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout27
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-macro-calls.fail.stdout60
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-macro-calls.pass.stdout9
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-macro-calls.rs44
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-stmts.rs33
-rw-r--r--tests/rustdoc-ui/doctest/main-alongside-stmts.stdout7
-rw-r--r--tests/ui/consts/const-eval/format.rs5
-rw-r--r--tests/ui/consts/const-eval/format.stderr10
-rw-r--r--tests/ui/linking/cdylib-no-mangle.rs20
-rw-r--r--tests/ui/linking/executable-no-mangle-strip.rs27
16 files changed, 274 insertions, 62 deletions
diff --git a/compiler/rustc_codegen_ssa/src/back/link.rs b/compiler/rustc_codegen_ssa/src/back/link.rs
index b59d73a9aae..5f170be354d 100644
--- a/compiler/rustc_codegen_ssa/src/back/link.rs
+++ b/compiler/rustc_codegen_ssa/src/back/link.rs
@@ -2011,6 +2011,12 @@ fn add_linked_symbol_object(
         file.set_mangling(object::write::Mangling::None);
     }
 
+    if file.format() == object::BinaryFormat::MachO {
+        // Divide up the sections into sub-sections via symbols for dead code stripping.
+        // Without this flag, unused `#[no_mangle]` or `#[used]` cannot be discard on MachO targets.
+        file.set_subsections_via_symbols();
+    }
+
     // ld64 requires a relocation to load undefined symbols, see below.
     // Not strictly needed if linking with lld, but might as well do it there too.
     let ld64_section_helper = if file.format() == object::BinaryFormat::MachO {
diff --git a/library/core/src/fmt/rt.rs b/library/core/src/fmt/rt.rs
index d27f7e6e0d8..eab48466599 100644
--- a/library/core/src/fmt/rt.rs
+++ b/library/core/src/fmt/rt.rs
@@ -205,8 +205,15 @@ impl Argument<'_> {
     /// let f = format_args!("{}", "a");
     /// println!("{f}");
     /// ```
+    ///
+    /// This function should _not_ be const, to make sure we don't accept
+    /// format_args!() and panic!() with arguments in const, even when not evaluated:
+    ///
+    /// ```compile_fail,E0015
+    /// const _: () = if false { panic!("a {}", "a") };
+    /// ```
     #[inline]
-    pub const fn none() -> [Self; 0] {
+    pub fn none() -> [Self; 0] {
         []
     }
 }
diff --git a/src/bootstrap/src/core/build_steps/test.rs b/src/bootstrap/src/core/build_steps/test.rs
index 81f6b473c45..0948a880827 100644
--- a/src/bootstrap/src/core/build_steps/test.rs
+++ b/src/bootstrap/src/core/build_steps/test.rs
@@ -2570,9 +2570,9 @@ fn prepare_cargo_test(
     // We skip everything on Miri as then this overwrites the libdir set up
     // by `Cargo::new` and that actually makes things go wrong.
     if builder.kind != Kind::Miri {
-        let mut dylib_path = dylib_path();
-        dylib_path.insert(0, PathBuf::from(&*builder.sysroot_target_libdir(compiler, target)));
-        cargo.env(dylib_path_var(), env::join_paths(&dylib_path).unwrap());
+        let mut dylib_paths = builder.rustc_lib_paths(compiler);
+        dylib_paths.push(PathBuf::from(&builder.sysroot_target_libdir(compiler, target)));
+        helpers::add_dylib_path(dylib_paths, &mut cargo);
     }
 
     if builder.remote_tested(target) {
diff --git a/src/librustdoc/doctest/make.rs b/src/librustdoc/doctest/make.rs
index 56b1e76ae8c..ea9b686c080 100644
--- a/src/librustdoc/doctest/make.rs
+++ b/src/librustdoc/doctest/make.rs
@@ -301,8 +301,6 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
 
     let filename = FileName::anon_source_code(&wrapped_source);
 
-    // Any errors in parsing should also appear when the doctest is compiled for real, so just
-    // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
     let sm = Arc::new(SourceMap::new(FilePathMapping::empty()));
     let fallback_bundle = rustc_errors::fallback_fluent_bundle(
         rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
@@ -311,7 +309,8 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
     info.supports_color =
         HumanEmitter::new(stderr_destination(ColorConfig::Auto), fallback_bundle.clone())
             .supports_color();
-
+    // Any errors in parsing should also appear when the doctest is compiled for real, so just
+    // send all the errors that the parser emits directly into a `Sink` instead of stderr.
     let emitter = HumanEmitter::new(Box::new(io::sink()), fallback_bundle);
 
     // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
@@ -339,9 +338,6 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
         *prev_span_hi = hi;
     }
 
-    // Recurse through functions body. It is necessary because the doctest source code is
-    // wrapped in a function to limit the number of AST errors. If we don't recurse into
-    // functions, we would thing all top-level items (so basically nothing).
     fn check_item(item: &ast::Item, info: &mut ParseSourceInfo, crate_name: &Option<&str>) -> bool {
         let mut is_extern_crate = false;
         if !info.has_global_allocator
@@ -351,8 +347,6 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
         }
         match item.kind {
             ast::ItemKind::Fn(_) if !info.has_main_fn => {
-                // We only push if it's the top item because otherwise, we would duplicate
-                // its content since the top-level item was already added.
                 if item.ident.name == sym::main {
                     info.has_main_fn = true;
                 }
@@ -411,37 +405,46 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
                     push_to_s(&mut info.crate_attrs, source, attr.span, &mut prev_span_hi);
                 }
             }
+            let mut has_non_items = false;
             for stmt in &body.stmts {
                 let mut is_extern_crate = false;
                 match stmt.kind {
                     StmtKind::Item(ref item) => {
-                        is_extern_crate = check_item(&item, &mut info, crate_name);
-                    }
-                    StmtKind::Expr(ref expr) if matches!(expr.kind, ast::ExprKind::Err(_)) => {
-                        reset_error_count(&psess);
-                        return Err(());
+                        is_extern_crate = check_item(item, &mut info, crate_name);
                     }
-                    StmtKind::MacCall(ref mac_call) if !info.has_main_fn => {
-                        let mut iter = mac_call.mac.args.tokens.iter();
-
-                        while let Some(token) = iter.next() {
-                            if let TokenTree::Token(token, _) = token
-                                && let TokenKind::Ident(name, _) = token.kind
-                                && name == kw::Fn
-                                && let Some(TokenTree::Token(fn_token, _)) = iter.peek()
-                                && let TokenKind::Ident(fn_name, _) = fn_token.kind
-                                && fn_name == sym::main
-                                && let Some(TokenTree::Delimited(_, _, Delimiter::Parenthesis, _)) = {
-                                    iter.next();
-                                    iter.peek()
+                    // We assume that the macro calls will expand to item(s) even though they could
+                    // expand to statements and expressions.
+                    StmtKind::MacCall(ref mac_call) => {
+                        if !info.has_main_fn {
+                            // For backward compatibility, we look for the token sequence `fn main(…)`
+                            // in the macro input (!) to crudely detect main functions "masked by a
+                            // wrapper macro". For the record, this is a horrible heuristic!
+                            // See <https://github.com/rust-lang/rust/issues/56898>.
+                            let mut iter = mac_call.mac.args.tokens.iter();
+                            while let Some(token) = iter.next() {
+                                if let TokenTree::Token(token, _) = token
+                                    && let TokenKind::Ident(kw::Fn, _) = token.kind
+                                    && let Some(TokenTree::Token(ident, _)) = iter.peek()
+                                    && let TokenKind::Ident(sym::main, _) = ident.kind
+                                    && let Some(TokenTree::Delimited(.., Delimiter::Parenthesis, _)) = {
+                                        iter.next();
+                                        iter.peek()
+                                    }
+                                {
+                                    info.has_main_fn = true;
+                                    break;
                                 }
-                            {
-                                info.has_main_fn = true;
-                                break;
                             }
                         }
                     }
-                    _ => {}
+                    StmtKind::Expr(ref expr) => {
+                        if matches!(expr.kind, ast::ExprKind::Err(_)) {
+                            reset_error_count(&psess);
+                            return Err(());
+                        }
+                        has_non_items = true;
+                    }
+                    StmtKind::Let(_) | StmtKind::Semi(_) | StmtKind::Empty => has_non_items = true,
                 }
 
                 // Weirdly enough, the `Stmt` span doesn't include its attributes, so we need to
@@ -466,6 +469,11 @@ fn parse_source(source: &str, crate_name: &Option<&str>) -> Result<ParseSourceIn
                     push_to_s(&mut info.crates, source, span, &mut prev_span_hi);
                 }
             }
+            if has_non_items {
+                // FIXME: if `info.has_main_fn` is `true`, emit a warning here to mention that
+                // this code will not be called.
+                info.has_main_fn = false;
+            }
             Ok(info)
         }
         Err(e) => {
diff --git a/tests/rustdoc-ui/doctest/auxiliary/items.rs b/tests/rustdoc-ui/doctest/auxiliary/items.rs
new file mode 100644
index 00000000000..40d4eb261e5
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/auxiliary/items.rs
@@ -0,0 +1 @@
+fn item() {}
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs
index 508faadcf67..ca5dd787467 100644
--- a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs
+++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.rs
@@ -4,12 +4,12 @@
 //@ compile-flags:--test
 //@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
 //@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
-//@ failure-status: 101
+//@ check-pass
 
 /// <https://github.com/rust-lang/rust/issues/91014>
 ///
 /// ```rust
-/// struct S {}; // unexpected semicolon after struct def
+/// struct S {};
 ///
 /// fn main() {
 ///    assert_eq!(0, 1);
diff --git a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout
index 9eb8b391e78..1068b98cb0f 100644
--- a/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout
+++ b/tests/rustdoc-ui/doctest/failed-doctest-extra-semicolon-on-item.stdout
@@ -1,29 +1,6 @@
 
 running 1 test
-test $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 11) ... FAILED
+test $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 11) ... ok
 
-failures:
-
----- $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 11) stdout ----
-error: expected item, found `;`
-  --> $DIR/failed-doctest-extra-semicolon-on-item.rs:12:12
-   |
-LL | struct S {}; // unexpected semicolon after struct def
-   |            ^
-   |
-   = help: braced struct declarations are not followed by a semicolon
-help: remove this semicolon
-   |
-LL - struct S {}; // unexpected semicolon after struct def
-LL + struct S {} // unexpected semicolon after struct def
-   |
-
-error: aborting due to 1 previous error
-
-Couldn't compile the test.
-
-failures:
-    $DIR/failed-doctest-extra-semicolon-on-item.rs - m (line 11)
-
-test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
 
diff --git a/tests/rustdoc-ui/doctest/main-alongside-macro-calls.fail.stdout b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.fail.stdout
new file mode 100644
index 00000000000..65989a8ef47
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.fail.stdout
@@ -0,0 +1,60 @@
+
+running 4 tests
+test $DIR/main-alongside-macro-calls.rs - (line 19) ... ok
+test $DIR/main-alongside-macro-calls.rs - (line 24) ... ok
+test $DIR/main-alongside-macro-calls.rs - (line 28) ... FAILED
+test $DIR/main-alongside-macro-calls.rs - (line 33) ... FAILED
+
+failures:
+
+---- $DIR/main-alongside-macro-calls.rs - (line 28) stdout ----
+error: macros that expand to items must be delimited with braces or followed by a semicolon
+  --> $DIR/main-alongside-macro-calls.rs:30:1
+   |
+LL | println!();
+   | ^^^^^^^^^^
+   |
+   = note: this error originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: macro expansion ignores `{` and any tokens following
+  --> $SRC_DIR/std/src/macros.rs:LL:COL
+   |
+  ::: $DIR/main-alongside-macro-calls.rs:30:1
+   |
+LL | println!();
+   | ---------- caused by the macro expansion here
+   |
+   = note: the usage of `print!` is likely invalid in item context
+
+error: aborting due to 2 previous errors
+
+Couldn't compile the test.
+---- $DIR/main-alongside-macro-calls.rs - (line 33) stdout ----
+error: macros that expand to items must be delimited with braces or followed by a semicolon
+  --> $DIR/main-alongside-macro-calls.rs:34:1
+   |
+LL | println!();
+   | ^^^^^^^^^^
+   |
+   = note: this error originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
+
+error: macro expansion ignores `{` and any tokens following
+  --> $SRC_DIR/std/src/macros.rs:LL:COL
+   |
+  ::: $DIR/main-alongside-macro-calls.rs:34:1
+   |
+LL | println!();
+   | ---------- caused by the macro expansion here
+   |
+   = note: the usage of `print!` is likely invalid in item context
+
+error: aborting due to 2 previous errors
+
+Couldn't compile the test.
+
+failures:
+    $DIR/main-alongside-macro-calls.rs - (line 28)
+    $DIR/main-alongside-macro-calls.rs - (line 33)
+
+test result: FAILED. 2 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/main-alongside-macro-calls.pass.stdout b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.pass.stdout
new file mode 100644
index 00000000000..93a4bbd8736
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.pass.stdout
@@ -0,0 +1,9 @@
+
+running 4 tests
+test $DIR/main-alongside-macro-calls.rs - (line 19) ... ok
+test $DIR/main-alongside-macro-calls.rs - (line 24) ... ok
+test $DIR/main-alongside-macro-calls.rs - (line 28) - compile fail ... ok
+test $DIR/main-alongside-macro-calls.rs - (line 33) - compile fail ... ok
+
+test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/rustdoc-ui/doctest/main-alongside-macro-calls.rs b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.rs
new file mode 100644
index 00000000000..b455d8b0cc3
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-macro-calls.rs
@@ -0,0 +1,44 @@
+// This test ensures that if there is are any macro calls alongside a `main` function,
+// it will indeed consider the `main` function as the program entry point and *won't*
+// generate its own `main` function to wrap everything even though macro calls are
+// valid in statement contexts, too, and could just as well expand to statements or
+// expressions (we don't perform any macro expansion to find `main`, see also
+// <https://github.com/rust-lang/rust/issues/57415>).
+//
+// See <./main-alongside-stmts.rs> for comparison.
+//
+//@ compile-flags:--test --test-args --test-threads=1
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ revisions: pass fail
+//@[pass] check-pass
+//@[fail] failure-status: 101
+
+// Regression test for <https://github.com/rust-lang/rust/pull/140220#issuecomment-2831872920>:
+
+//! ```
+//! fn main() {}
+//! include!("./auxiliary/items.rs");
+//! ```
+//!
+//! ```
+//! include!("./auxiliary/items.rs");
+//! fn main() {}
+//! ```
+
+// Regression test for <https://github.com/rust-lang/rust/issues/140412>:
+// We test the "same" thing twice: Once via `compile_fail` to more closely mirror the reported
+// regression and once without it to make sure that it leads to the expected rustc errors,
+// namely `println!(…)` not being valid in item contexts.
+
+#![cfg_attr(pass, doc = " ```compile_fail")]
+#![cfg_attr(fail, doc = " ```")]
+//! fn main() {}
+//! println!();
+//! ```
+//!
+#![cfg_attr(pass, doc = " ```compile_fail")]
+#![cfg_attr(fail, doc = " ```")]
+//! println!();
+//! fn main() {}
+//! ```
diff --git a/tests/rustdoc-ui/doctest/main-alongside-stmts.rs b/tests/rustdoc-ui/doctest/main-alongside-stmts.rs
new file mode 100644
index 00000000000..5965f928cdd
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-stmts.rs
@@ -0,0 +1,33 @@
+// This test ensures that if there is are any statements alongside a `main` function,
+// it will not consider the `main` function as the program entry point but instead
+// will generate its own `main` function to wrap everything as it needs to reside in a
+// module where only *items* are permitted syntactically.
+//
+// See <./main-alongside-macro-calls.rs> for comparison.
+//
+// This is a regression test for:
+// * <https://github.com/rust-lang/rust/issues/140162>
+// * <https://github.com/rust-lang/rust/issues/139651>
+//
+//@ compile-flags:--test --test-args --test-threads=1
+//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"
+//@ normalize-stdout: "finished in \d+\.\d+s" -> "finished in $$TIME"
+//@ check-pass
+
+//! ```
+//! # if cfg!(miri) { return; }
+//! use std::ops::Deref;
+//!
+//! fn main() {
+//!     assert!(false);
+//! }
+//! ```
+//!
+//! ```
+//! let x = 2;
+//! assert_eq!(x, 2);
+//!
+//! fn main() {
+//!     assert!(false);
+//! }
+//! ```
diff --git a/tests/rustdoc-ui/doctest/main-alongside-stmts.stdout b/tests/rustdoc-ui/doctest/main-alongside-stmts.stdout
new file mode 100644
index 00000000000..9b9a3fe8a68
--- /dev/null
+++ b/tests/rustdoc-ui/doctest/main-alongside-stmts.stdout
@@ -0,0 +1,7 @@
+
+running 2 tests
+test $DIR/main-alongside-stmts.rs - (line 17) ... ok
+test $DIR/main-alongside-stmts.rs - (line 26) ... ok
+
+test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in $TIME
+
diff --git a/tests/ui/consts/const-eval/format.rs b/tests/ui/consts/const-eval/format.rs
index 1878fc03827..a8085a786e1 100644
--- a/tests/ui/consts/const-eval/format.rs
+++ b/tests/ui/consts/const-eval/format.rs
@@ -9,4 +9,9 @@ const fn print() {
     //~| ERROR cannot call non-const function `_print` in constant functions
 }
 
+const fn format_args() {
+    format_args!("{}", 0);
+    //~^ ERROR cannot call non-const formatting macro in constant functions
+}
+
 fn main() {}
diff --git a/tests/ui/consts/const-eval/format.stderr b/tests/ui/consts/const-eval/format.stderr
index e8d7bbcea09..4c4cbb372a7 100644
--- a/tests/ui/consts/const-eval/format.stderr
+++ b/tests/ui/consts/const-eval/format.stderr
@@ -24,6 +24,14 @@ LL |     println!("{:?}", 0);
    = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
    = note: this error originates in the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
 
-error: aborting due to 3 previous errors
+error[E0015]: cannot call non-const formatting macro in constant functions
+  --> $DIR/format.rs:13:5
+   |
+LL |     format_args!("{}", 0);
+   |     ^^^^^^^^^^^^^^^^^^^^^
+   |
+   = note: calls in constant functions are limited to constant functions, tuple structs and tuple variants
+
+error: aborting due to 4 previous errors
 
 For more information about this error, try `rustc --explain E0015`.
diff --git a/tests/ui/linking/cdylib-no-mangle.rs b/tests/ui/linking/cdylib-no-mangle.rs
new file mode 100644
index 00000000000..f442c3f584d
--- /dev/null
+++ b/tests/ui/linking/cdylib-no-mangle.rs
@@ -0,0 +1,20 @@
+//@ only-apple
+//@ build-fail
+//@ dont-check-compiler-stderr
+//@ dont-check-compiler-stdout
+
+// Regression test for <https://github.com/rust-lang/rust/issues/139744>.
+// Functions in the dynamic library marked with no_mangle should not be GC-ed.
+
+#![crate_type = "cdylib"]
+
+unsafe extern "C" {
+    unsafe static THIS_SYMBOL_SHOULD_BE_UNDEFINED: usize;
+}
+
+#[unsafe(no_mangle)]
+pub unsafe fn function_marked_with_no_mangle() {
+    println!("FUNCTION_MARKED_WITH_NO_MANGLE = {}", unsafe { THIS_SYMBOL_SHOULD_BE_UNDEFINED });
+}
+
+//~? ERROR linking
diff --git a/tests/ui/linking/executable-no-mangle-strip.rs b/tests/ui/linking/executable-no-mangle-strip.rs
new file mode 100644
index 00000000000..cc283dc53ee
--- /dev/null
+++ b/tests/ui/linking/executable-no-mangle-strip.rs
@@ -0,0 +1,27 @@
+//@ run-pass
+//@ ignore-windows-gnu: only statics marked with used can be GC-ed on windows-gnu
+
+// Regression test for <https://github.com/rust-lang/rust/issues/139744>.
+// Functions in the binary marked with no_mangle should be GC-ed if they
+// are not indirectly referenced by main.
+
+#![feature(used_with_arg)]
+
+#[cfg_attr(windows, link(name = "this_lib_does_not_exist", kind = "raw-dylib"))]
+unsafe extern "C" {
+    unsafe static THIS_SYMBOL_SHOULD_BE_UNDEFINED: usize;
+}
+
+#[unsafe(no_mangle)]
+pub unsafe fn function_marked_with_no_mangle() {
+    println!("FUNCTION_MARKED_WITH_NO_MANGLE = {}", unsafe { THIS_SYMBOL_SHOULD_BE_UNDEFINED });
+}
+
+#[used(compiler)]
+pub static FUNCTION_MARKED_WITH_USED: unsafe fn() = || {
+    println!("FUNCTION_MARKED_WITH_USED = {}", unsafe { THIS_SYMBOL_SHOULD_BE_UNDEFINED });
+};
+
+fn main() {
+    println!("MAIN");
+}