Skip to content

Conversation

@Shourya742
Copy link
Contributor

@Shourya742 Shourya742 commented Dec 11, 2025

This PR adds a bidirectional proc-macro protocol so the server can ask the client for data while expanding a macro . The approach is still synchronous and single-request-at-a-time. The server may emit sub-requests during expansion, the client replies immediately, and the server then finishes with the final response. All messages use the same request id, and stdin/stdout remain strictly ordered. Legacy protocols are untouched

@Shourya742 Shourya742 force-pushed the 2025-11-27-bidirectional-protocol branch 4 times, most recently from 0a41ccb to bb61e2e Compare December 11, 2025 17:12
@Shourya742 Shourya742 force-pushed the 2025-11-27-bidirectional-protocol branch from bb61e2e to 376bca9 Compare December 15, 2025 14:00
@Shourya742 Shourya742 force-pushed the 2025-11-27-bidirectional-protocol branch from 339c074 to 57fdf52 Compare December 18, 2025 08:36
@Shourya742 Shourya742 force-pushed the 2025-11-27-bidirectional-protocol branch from a01d06d to 19e816d Compare December 18, 2025 09:17
@Shourya742 Shourya742 marked this pull request as ready for review December 18, 2025 09:34
@rustbot rustbot added the S-waiting-on-review Status: Awaiting review from the assignee but also interested parties. label Dec 18, 2025
Copy link
Contributor

@ChayimFriedman2 ChayimFriedman2 left a comment

Choose a reason for hiding this comment

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

Overall good code, left some comments.

@Veykril would you like to review this too?

Comment on lines +20 to +24
impl From<(RequestId, Kind, Payload)> for Envelope {
fn from(value: (RequestId, Kind, Payload)) -> Self {
Envelope { id: value.0, kind: value.1, payload: value.2 }
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
impl From<(RequestId, Kind, Payload)> for Envelope {
fn from(value: (RequestId, Kind, Payload)) -> Self {
Envelope { id: value.0, kind: value.1, payload: value.2 }
}
}

This is not used (and also not a good practice).

legacy_protocol::msg::{FlatTree, Message, PanicMessage, ServerConfig},
};

pub type RequestId = u32;
Copy link
Contributor

Choose a reason for hiding this comment

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

4 billion requests are a lot but still in the realm of possibility for this to overflow for a long-lived rust-analyzer process with a lot of macros, so I think better to have this as a u64.

Also, a newtype, maybe?

pub(crate) enum Protocol {
LegacyJson { mode: SpanMode },
LegacyPostcard { mode: SpanMode },
NewPostcard { mode: SpanMode },
Copy link
Contributor

Choose a reason for hiding this comment

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

Better to call this BidirectionalPostcard and ditto for NewJson, so we won't have to rename again when we replace it (and also it's consistent with the module names).

Copy link
Member

Choose a reason for hiding this comment

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

Let's call it BidirectionalPostcardPrototype (and similar for the cli flags). I expect us to break this a bit here and there so i want us to differentiate that.

In part because i will likely have another look at the proto definitions in general and restructure them

match self.protocol {
Protocol::LegacyJson { mode } => mode == SpanMode::RustAnalyzer,
Protocol::LegacyPostcard { mode } => mode == SpanMode::RustAnalyzer,
Protocol::NewJson { mode } => mode == SpanMode::RustAnalyzer,
Copy link
Contributor

Choose a reason for hiding this comment

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

@Veykril will the Jetbrains people want to use the new protocol (but not rust-analyzer span mode)? If not, it makes sense to remove the token id span+new protocols combination completely, unless it's too much work.

Copy link
Member

Choose a reason for hiding this comment

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

I haven't talked to them yet regarding this, but for now I think we should only do the postcard one for simplicity for now.

tracing.workspace = true
rustc-hash.workspace = true
indexmap.workspace = true
base-db.workspace = true
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally we really do not want to bring in salsa and base-db and friends here, but given that we're going to get rid of this completely after this RP (#t-compiler/rust-analyzer > Non-generic spans), I suppose it's fine?

}
}

pub enum SubRequest {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason you need to redefine this enum here, instead of just reuse it?

});

let (subreq_tx, subreq_rx) = unbounded::<proc_macro_srv::SubRequest>();
let (subresp_tx, subresp_rx) = unbounded::<proc_macro_srv::SubResponse>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Generally it's better for channels to be bounded, this way a fast one side a slow other side can flood memory with requests.

result
}

pub fn expand_with_channels<S: ProcMacroSrvSpan>(
Copy link
Contributor

Choose a reason for hiding this comment

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

There's some code reuse potential with expand() here, perhaps take a callback.

Err(bridge::PanicMessage::String(format!("proc-macro `{macro_name}` is missing")).into())
}

pub(crate) fn expand_with_channels<S: ProcMacroSrvSpan>(
Copy link
Contributor

Choose a reason for hiding this comment

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

Make this take Option<Cahnnel>s and make expand() call it, to reuse code.


- name: Check salsa dependency
run: "! (cargo tree -p proc-macro-srv-cli | grep -q salsa)"

Copy link
Contributor

Choose a reason for hiding this comment

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

Better to comment this only temporarily with a FIXME until we do #t-compiler/rust-analyzer > Non-generic spans.

Comment on lines +155 to +156
cli_to_server: crossbeam_channel::Receiver<SubResponse>,
server_to_cli: crossbeam_channel::Sender<SubRequest>,
Copy link
Member

Choose a reason for hiding this comment

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

I would expect us to model this via a callback instead impl FnMut(SubRequest) -> Result<SubResponse>, modelling this with channels seems weird given us sending a request has to be blocking anyways

Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that it's structured this way so that it's easier to make it concurrent in the future.

Copy link
Member

Choose a reason for hiding this comment

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

That's fine, a single proc-macro execution will still run sequential so a callback works out there. The cli-server should manage the connection of the channels here, the proc-macro server impl should merely be able to say, I want to emit this subrequest, give me the corresponding reply: That's best modeled by a callback.

match self.protocol {
Protocol::LegacyJson { mode } => mode == SpanMode::RustAnalyzer,
Protocol::LegacyPostcard { mode } => mode == SpanMode::RustAnalyzer,
Protocol::NewJson { mode } => mode == SpanMode::RustAnalyzer,
Copy link
Member

Choose a reason for hiding this comment

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

I haven't talked to them yet regarding this, but for now I think we should only do the postcard one for simplicity for now.

version: 0,
protocol: protocol.clone(),
exited: OnceLock::new(),
next_request_id: AtomicU32::new(1),
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need this for now? This PR should only allow blocking bidirectionality, no concurrency so we don't run into the risk of having multiple requests outstanding. If we do, havent checked the server-cli loop code yet then we should move that out of this PR and only focus on bidirectionality for now

Comment on lines +213 to +229
struct Callbacks<'de> {
db: &'de dyn SourceDatabase,
}
impl<'db> ClientCallbacks for Callbacks<'db> {
fn handle_sub_request(&mut self, req: SubRequest) -> Result<SubResponse, ServerError> {
match req {
SubRequest::SourceText { file_id, start, end } => {
let file = FileId::from_raw(file_id);
let text = self.db.file_text(file).text(self.db);

let slice = text.get(start as usize..end as usize).map(|s| s.to_owned());

Ok(SubResponse::SourceTextResult { text: slice })
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

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

This to me feels like the wrong place to put this. I think the handler implementations need to happen in the client side crate, that is rust-analyzer. That will also allow us to remove the unwanted dependency on base-db in here. This code shouldn't need to know about databases and such.

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

Labels

S-waiting-on-review Status: Awaiting review from the assignee but also interested parties.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants