Skip to content

Make #[track_caller] async fn track the caller, not the poller/awaiter - #163396

Open
theemathas wants to merge 7 commits into
rust-lang:mainfrom
theemathas:async-track-caller-not-poller
Open

theemathas wants to merge 7 commits into
rust-lang:mainfrom
theemathas:async-track-caller-not-poller

Conversation

@theemathas

@theemathas theemathas commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

View all comments

An LLM was used to locate relevant code, suggest ideas, and review the code. However, I manually wrote all code, and I take all responsibility for all code written and all decisions made.

Tracking issue (async_fn_track_caller): #110011

Related tracking issue (closure_track_caller): #87417

This PR is stacked on top of #163262 (first commit in this PR). The second commit adds extensive tests for behavior touched in this PR. Commits 3-7 are the actual implementation, and also modification of the tests to reflect this new behavior.

This PR makes coroutines desugared from #[track_caller] async fn track the caller of the function, not the poller/awaiter of the coroutine. This is done as per T-lang's decision at #110011 (comment). The implementation modifies AST lowering to add an upvar to the coroutine, which stores the relevant caller location.

This PR changes the behavior of async fn, but not of async closures, due to difficulties I've noted at #t-compiler/help > Help with `async_fn_track_caller` @ 💬

This PR's implementation assumes that the desired behavior in #110011 (comment) is the behavior that I think makes the most sense.

This PR corrects the behavior of #163406 when the async_fn_track_caller feature is enabled, but does not give a warning when the feature is disabled (resulting in the pre-existing broken behavior).

r? compiler

@rustbot rustbot added A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Sep 27, 2026
@theemathas
theemathas force-pushed the async-track-caller-not-poller branch 3 times, most recently from f21a754 to 7e01c99 Compare September 28, 2026 15:47
@rust-log-analyzer

This comment has been minimized.

@rust-bors

This comment has been minimized.

@theemathas
theemathas force-pushed the async-track-caller-not-poller branch from 7e01c99 to c87bff0 Compare September 29, 2026 11:52
Comment on lines +988 to +997
if is_in_trait_impl {
// Check if we need to "inherit" #[track_caller] from the trait definition.
let Some(trait_item_def_id) =
self.get_partial_res(node_id).and_then(|r| r.expect_full_res().opt_def_id())
else {
self.dcx().span_delayed_bug(span, "could not resolve trait item being implemented");
return false;
};
return find_attr!(self.tcx, trait_item_def_id, TrackCaller(_));
}

@theemathas theemathas Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what this is doing, but I'm doing the same thing as this existing code:

let (effective_ident, impl_kind) = if is_in_trait_impl {
let trait_item_def_id = self
.get_partial_res(i.id)
.and_then(|r| r.expect_full_res().opt_def_id())
.ok_or_else(|| {
self.dcx()
.span_delayed_bug(span, "could not resolve trait item being implemented")
});

View changes since the review

Comment on lines +77 to +87
// We currently do not support inlining a callee which is the coroutine
// desugared from `#[track_caller] async fn`.
// This is because figuring out the caller_location of such coroutines
// requires accessing the argument of the `poll()` call. And inlining
// would cause us to lose track of where that argument is.
if let Some(coroutine) = &body.coroutine
&& coroutine.captured_caller_location.is_some()
{
return Err("can't inline coroutines from `#[track_caller] async fn`");
}

@theemathas theemathas Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests continue to pass even after I remove this code. Am I missing something? How do I test that this code is doing what it's supposed to?

View changes since the review

Comment on lines +108 to 110
///
/// FIXME(async_fn_track_caller): What if the Location is stored inside a coroutine upvar?
fn resolve_tracked_call_location(body: &Body, inherited: Option<MirConst>) -> MirConst {

@theemathas theemathas Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this, and do I need to modify it?

View changes since the review

@theemathas
theemathas marked this pull request as ready for review September 29, 2026 12:19
@rustbot

rustbot commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

miri is developed in its own repository. If the Miri part of this change can be broken out, consider making this change to rust-lang/miri instead. However, if Miri needs adjusting for rustc changes, just ignore this message.

cc @rust-lang/miri

Some changes occurred to the CTFE machinery

cc @RalfJung, @oli-obk, @lcnr

Some changes occurred in compiler/rustc_attr_ir

cc @jdonszelmann, @JonathanBrouwer

Some changes occurred to MIR optimizations

cc @rust-lang/wg-mir-opt

Some changes occurred to the intrinsics. Make sure the CTFE / Miri interpreter
gets adapted for the changes, if necessary.

cc @rust-lang/miri, @RalfJung, @oli-obk, @lcnr

Some changes occurred to the CTFE / Miri interpreter

cc @rust-lang/miri

@rustbot rustbot added S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Sep 29, 2026
/// The span used in errors if we can't read the captured location.
fallback_span: Span,
},
}

@RalfJung RalfJung Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AFAIK this only exists to support the intrinsic, so please move the definition into the intrinsic file.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what you mean. The type is used in three places:

  • Return type of find_closest_untracked_caller_location (this file)
  • Consumed in caller_location, which is then used in things other than the intrinsic (this file)
  • Consumed in type_implements_dyn_trait to produce a span_bug. (compiler/rustc_const_eval/src/interpret/util.rs)

@RalfJung RalfJung Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We said that the interpreter stacktrace doesn't use the async fn caller location, right? So this should only be used inside the intrinsic then.

Consumed in type_implements_dyn_trait to produce a span_bug. (compiler/rustc_const_eval/src/interpret/util.rs)

That definitely shouldn't do anything complicated involving coroutines. It's just a bug location. It should probably use ecx.cur_span(); no idea why a more complicated span computation was used there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The stacktrace when UB happens doesn't use the async fn caller location. However, the caller location for panics and Location::caller() do read from the interpreted program's memory. I think it makes sense that the behavior in non-UB cases should match the behavior outside Miri.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the caller location for panics

That uses the intrinsic internally.

So, it should only be the intrinsic that uses the new logic.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That uses the intrinsic internally.

Not always. See the changes in this PR in compiler/rustc_const_eval/src/const_eval/machine.rs

@RalfJung RalfJung Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That still conceptually uses the intrinsic, it's just doing the backtrace printing hard-coded in the interpreter.

Arguably it could keep using the old logic for now, const-eval can't run async functions anyway. That part of your changes is completely untested I think.

Comment on lines +736 to +738
/// Read from a local of the specified frame.
/// Will not access memory, instead an indirect `Operand` is returned.
pub fn local_in_frame_to_op(

@RalfJung RalfJung Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an accidental revert of some recent changes?

EDIT: Oh no you're actually using this. That's almost certainly wrong, you need a proper pointer to access locals in other stack frames.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's wrong with this? The test seems to pass...

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does the non-interpreter version of this work, at a high level? What actually happens in the desugared state machine MIR?

@theemathas theemathas Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Completely differently from how Miri handles it. There's an upvar in the #[track_caller] async fn coroutine that stores the Location::caller, and that gets copied then passed around as an implicit argument through all the layers of functions. This happens in codegen, not in MIR. See https://rustc-dev-guide.rust-lang.org/backend/implicit-caller-location.html

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know how it works for non-coroutines, but the PR description doesn't explain how this is adjusted for deal with coroutines.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're saying that the #[track_caller] implementation in Miri needs to be entirely redone? 🫠

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternative: The caller location data is copied into a new read-only allocation, and that allocation is read each time miri needs the caller location from some downstream stack frame.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're saying that the #[track_caller] implementation in Miri needs to be entirely redone? 🫠

That might be necessary...

Alternative: The caller location data is copied into a new read-only allocation, and that allocation is read each time miri needs the caller location from some downstream stack frame.

When and how exactly is what exactly stored there and when is it read by which operation?

@theemathas theemathas Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right before a coroutine calls anything, the &Location value is copied to a new allocation inaccessible to user code. Miri walks up the stack and reads this allocation each time Miri needs the caller location while executing in a different stack frame. I'm not sure if this needs a retag though.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds quite similar to one half of what I wrote: when a coroutine calls a function, store the caller location in a field of the new stack frame. Except your version does it always, my version does it just when the callee is #[track_caller] as otherwise it's just not needed.

If we do this we end up with a hybrid system for track_caller handling, where it's partially handled like codegen and partially not. I think we should then move to fully handling it more like codegen, but I am fine with deferring the 2nd half of this to a later PR if you think that the hybrid thing is easier to implement.

@theemathas
theemathas force-pushed the async-track-caller-not-poller branch from c87bff0 to b14e6ed Compare September 29, 2026 13:07
Comment on lines +302 to 311
either::Either::Right(place) => self
.mplace_to_imm_ptr(
&place,
Some(
self.tcx.erase_and_anonymize_regions(self.tcx.caller_location_ty()),
),
)?
.into(),
};
self.copy_op(&val, dest)?;

@RalfJung RalfJung Sep 29, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will do actual reads from memory now, from the field in the coroutine that holds the location. I don't think that that's accurate in terms of modeling which UB should happen when.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh hmmm.... you're probably right. It would probably be different in cases where, for whatever reason, the caller location field is overwritten after the coroutine has already started executing. I'm not sure yet though how to fix this...

@theemathas

Copy link
Copy Markdown
Contributor Author

Do I need to handle cranelift in this PR?


// Get the location information.
let location = self.get_caller_location(bx, terminator.source_info).immediate();
let location = self.codegen_caller_location(bx, terminator.source_info).immediate();

@theemathas theemathas Sep 30, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@RalfJung Is the exact order where this is called (relative to codegenning other parameters) important? (And similarly for the other codegen_caller_location call later in this file.)

View changes since the review

Previously, in rust-lang#161972,
this test was skipped on gcc, due to a bug in rustc_codegen_gcc.
The bug was fixed by the subtree update at
rust-lang#162499.
@theemathas
theemathas force-pushed the async-track-caller-not-poller branch from b14e6ed to e1d818f Compare October 1, 2026 13:57
@rustbot

rustbot commented Oct 1, 2026

Copy link
Copy Markdown
Collaborator

This PR was rebased onto a different main commit. Here's a range-diff highlighting what actually changed.

Rebasing is a normal part of keeping PRs up to date, so no action is needed—this note is just to help reviewers.

@theemathas
theemathas force-pushed the async-track-caller-not-poller branch from e1d818f to 6fcdc09 Compare October 1, 2026 14:56
@theemathas

Copy link
Copy Markdown
Contributor Author

I split up the implementation into multiple commits, hopefully making reviewing easier. (And also hopefully making it less confusing for me.)

@theemathas
theemathas force-pushed the async-track-caller-not-poller branch 2 times, most recently from 6fcdc09 to 473dbf0 Compare October 1, 2026 15:01
@theemathas theemathas changed the title Make #[track_caller] async fn track the caller, not the poller Make #[track_caller] async fn track the caller, not the poller/awaiter Oct 2, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-attributes Area: Attributes (`#[…]`, `#![…]`) S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants