From b3361ee2a9852c299aac7ad1480ce3db943bef84 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Wed, 2 Sep 2026 05:03:48 +0800 Subject: [PATCH] feat: codegen for the Reply-based Dispatch + Call addressing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Catches comline-codegen-rust up to runtime#10 (pluggable framing): - the generated Dispatcher gains `fn calls(&self) -> &'static [&'static str]` (returns _CALLS) so a name-oriented framing can resolve a method to an ordinal. - `dispatch` takes `reply: &mut Reply` instead of `out: &mut dyn BufMut`; arms call `reply.ok(&body)` / `reply.err(ord, &body)` instead of `Envelope::encode_*`. The handler-return binding is `value` now (was `reply`, which would shadow the param). A fully one-way protocol names the param `_reply`. - the client stub passes `Call::new(i, "name")` (both addresses) to `call` / `call_with_timeout` / `notify`, not a bare `i u16`. - import line: `BufMut` -> `Call, Reply`. Bumps the generated crate's comline-runtime pin. tests/compiles.rs builds the generated crate against the new runtime; generate.rs string assertions updated. Behaviour is still datagram framing — a framing *selector* on the generated connect/serve helpers is a follow-up. The conformance corpus golden re-bless follows separately. --- codegen/src/generator.rs | 43 +++++++++++++++++++++++---------------- codegen/tests/generate.rs | 21 +++++++++++-------- 2 files changed, 38 insertions(+), 26 deletions(-) diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index e9f27d2..72f4c0c 100644 --- a/codegen/src/generator.rs +++ b/codegen/src/generator.rs @@ -20,7 +20,7 @@ use comline_codegen::{GenRequest, GeneratedFile, Mode, PackageMeta}; /// `comline-runtime` — the crate the generated RPC code links against. Pinned /// by git rev (no crates.io yet); see design/runtime-repo-structure.md. const RUNTIME_GIT: &str = "https://github.com/ComlineProject/runtime"; -const RUNTIME_REV: &str = "3689cfdc480dc6b183b8ca33ade0a3cdf1a0d97c"; +const RUNTIME_REV: &str = "0dda42de5105e75e339bf66fa38085bd53ab9ab9"; pub fn generate_rust(req: &GenRequest) -> Result> { match req.mode { @@ -118,7 +118,7 @@ fn schema_source(units: &[FrozenUnit]) -> String { output.push_str( "use comline_runtime::client::Client;\n\ use comline_runtime::contract::{\n \ - BufMut, CallError, Dispatch, Envelope, Handshake, Kind, RuntimeError,\n \ + Call, CallError, Dispatch, Envelope, Handshake, Kind, Reply, RuntimeError,\n \ WireFormat, FRAMING_DATAGRAM,\n\ };\n\ use comline_runtime::serve::Server;\n\ @@ -380,16 +380,24 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap let mut l: Vec = Vec::new(); l.push(format!("pub struct {proto}Dispatcher(pub S);")); l.push(String::new()); - // `out` is only written by request/response arms; a protocol that is - // entirely one-way never touches it. - let out_param = if fns.iter().all(|f| f.one_way) { "_out" } else { "out" }; + // `reply` is only touched by request/response arms; a protocol that is + // entirely one-way never records anything. + let reply_param = if fns.iter().all(|f| f.one_way) { + "_reply" + } else { + "reply" + }; l.push(format!("impl Dispatch for {proto}Dispatcher {{")); + l.push(" fn calls(&self) -> &'static [&'static str] {".into()); + l.push(format!(" {calls_const}")); + l.push(" }".into()); + l.push(String::new()); l.push(" fn dispatch(".into()); l.push(" &self,".into()); l.push(" call: Kind,".into()); l.push(" params: &[u8],".into()); l.push(" fmt: &W,".into()); - l.push(format!(" {out_param}: &mut dyn BufMut,")); + l.push(format!(" {reply_param}: &mut Reply,")); l.push(" ) -> Result<(), RuntimeError> {".into()); l.push(format!( " match call.resolve({calls_const}).ok_or(RuntimeError::UnknownCall)? {{" @@ -412,23 +420,21 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap }; let call = format!("self.0.{}({call_args})", f.name); if f.one_way { - // Run the handler; write no envelope — the `Server` sees the - // empty buffer and replies with nothing. + // Run the handler; record nothing — the `Server` sees `Outcome::None` + // and replies with nothing. l.push(format!(" {call};")); } else { l.push(format!(" match {call} {{")); - l.push(" Ok(reply) => {".into()); + l.push(" Ok(value) => {".into()); l.push(" let mut body = Vec::new();".into()); - l.push(" fmt.encode(&reply, &mut body)?;".into()); - l.push(" Envelope::encode_ok(&body, out);".into()); + l.push(" fmt.encode(&value, &mut body)?;".into()); + l.push(" reply.ok(&body);".into()); l.push(" }".into()); for (ord, err) in &f.throws { l.push(format!(" Err({}::{err}(e)) => {{", f.err_ty)); l.push(" let mut body = Vec::new();".into()); l.push(" fmt.encode(&e, &mut body)?;".into()); - l.push(format!( - " Envelope::encode_err({ord}u16, &body, out);" - )); + l.push(format!(" reply.err({ord}u16, &body);")); l.push(" }".into()); } if f.throws.is_empty() { @@ -495,6 +501,9 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap } None => "&()".to_string(), }; + // Both addresses — the framing picks (ordinal for datagram, name for + // a name-oriented framing like JSON-RPC). + let addr = format!("Call::new({i}, \"{}\")", f.name); l.push(String::new()); if f.one_way { // Fire-and-forget: no reply, so no `CallError`. @@ -502,7 +511,7 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap " pub fn {}(&mut self{args}) -> Result<(), RuntimeError> {{", f.name )); - l.push(format!(" self.0.notify({i}u16, {param_expr})")); + l.push(format!(" self.0.notify({addr}, {param_expr})")); l.push(" }".into()); continue; } @@ -512,9 +521,9 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap )); let call = match f.timeout_ms { Some(ms) => format!( - "self.0.call_with_timeout({i}u16, {param_expr}, core::time::Duration::from_millis({ms}))" + "self.0.call_with_timeout({addr}, {param_expr}, core::time::Duration::from_millis({ms}))" ), - None => format!("self.0.call({i}u16, {param_expr})"), + None => format!("self.0.call({addr}, {param_expr})"), }; l.push(format!(" let (reply, fmt) = {call}?;")); l.push(" match reply {".into()); diff --git a/codegen/tests/generate.rs b/codegen/tests/generate.rs index 6d53108..ccf2e10 100644 --- a/codegen/tests/generate.rs +++ b/codegen/tests/generate.rs @@ -219,13 +219,16 @@ fn protocol_errors_map_to_ordinals_and_a_union() { // per-protocol union + From impl assert!(src.contains("pub enum ChatError {\n Rejected(Rejected),\n}")); assert!(src.contains("impl From for ChatError")); - // dispatcher encodes the error at its ordinal - assert!(src.contains("Envelope::encode_err(0u16, &body, out);")); + // dispatcher records the error at its ordinal on the framing-agnostic Reply + assert!(src.contains("reply.err(0u16, &body);")); + assert!(src.contains("reply.ok(&body);")); + assert!(src.contains("fn calls(&self) -> &'static [&'static str] {")); // client maps that ordinal back assert!(src.contains("Envelope::Err { id: 0u16, body } =>")); // `-> ()` (Unit) is request/response with an empty ack assert!(src.contains("fn ping(&self) -> Result<(), ChatPingError>;")); - assert!(src.contains("self.0.call(1u16, &())")); + // both addresses on the wire; the framing picks + assert!(src.contains(r#"self.0.call(Call::new(1, "ping"), &())"#)); // a `str` arg is borrowed assert!(src.contains("fn send(&self, body: &str) -> Result<(), ChatSendError>;")); assert!(src.contains("pub struct ChatSendParams<'a> {\n #[serde(borrow)]\n pub body: &'a str,\n}")); @@ -235,7 +238,7 @@ fn protocol_errors_map_to_ordinals_and_a_union() { assert!(!src.contains("ChatPokeError")); assert!(src.contains("fn poke(&self, note: &str);")); assert!(src.contains("pub fn poke(&mut self, note: &str) -> Result<(), RuntimeError> {")); - assert!(src.contains("self.0.notify(2u16, &ChatPokeParams { note })")); + assert!(src.contains(r#"self.0.notify(Call::new(2, "poke"), &ChatPokeParams { note })"#)); } #[test] @@ -275,13 +278,13 @@ fn timeout_ms_annotation_emits_call_with_timeout() { let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents; assert!(src.contains( - "self.0.call_with_timeout(0u16, &(), core::time::Duration::from_millis(2500))?;" + r#"self.0.call_with_timeout(Call::new(0, "slow"), &(), core::time::Duration::from_millis(2500))?;"# )); - assert!(src.contains("self.0.call(1u16, &())?;")); + assert!(src.contains(r#"self.0.call(Call::new(1, "fast"), &())?;"#)); } #[test] -fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() { +fn an_all_one_way_protocol_leaves_the_dispatch_reply_param_unbound() { let proto = FrozenUnit::Protocol { docstring: "Bus".to_string(), parameters: vec![], @@ -303,8 +306,8 @@ fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() { }; let schemas = vec![("bus".to_string(), vec![proto])]; let src = generate_rust(&code_req(&schemas)).unwrap().remove(0).contents; - assert!(src.contains("_out: &mut dyn BufMut,")); - assert!(!src.contains("\n out: &mut dyn BufMut,")); + assert!(src.contains("_reply: &mut Reply,")); + assert!(!src.contains("\n reply: &mut Reply,")); } #[test]