diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index a1d51bd..a465fb5 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 = "93af95d7f5944ceb65051d1236843b0daa2aa1f0"; +const RUNTIME_REV: &str = "435b7e833f970fadf4edbedcade20037143713d5"; pub fn generate_rust(req: &GenRequest) -> Result> { match req.mode { @@ -235,6 +235,11 @@ struct FnInfo { err_ty: String, /// `(ordinal, error struct type)` for each `!` on this function. throws: Vec<(u16, String)>, + /// `_return: None` — fire-and-forget: `Client::notify`, the dispatcher + /// writes no envelope, the trait method returns `()` with no error. An + /// explicit `-> ()` (`KindValue::Unit`) is *not* this — it's a normal + /// request/response with an empty ack (§4.4). + one_way: bool, } fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap) -> String { @@ -271,8 +276,12 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap } } - // 2. one schema-only error enum per function (empty when it throws nothing) + // 2. one schema-only error enum per request/response function (empty when + // it throws nothing). One-way functions have no error channel. for f in &fns { + if f.one_way { + continue; + } s.push_str(&format!( "#[derive(Debug, Clone, PartialEq)]\npub enum {} {{\n", f.err_ty @@ -315,21 +324,15 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap // 3. provider trait + the call table s.push_str(&format!("pub trait {proto} {{\n")); for f in &fns { - let args = f - .args - .iter() - .map(|a| format!("{}: {}", a.name, a.sig_ty)) - .collect::>() - .join(", "); - let args = if args.is_empty() { - String::new() + let args = sig_args(&f.args); + if f.one_way { + s.push_str(&format!(" fn {}(&self{args});\n", f.name)); } else { - format!(", {args}") - }; - s.push_str(&format!( - " fn {}(&self{args}) -> Result<{}, {}>;\n", - f.name, f.ret, f.err_ty - )); + s.push_str(&format!( + " fn {}(&self{args}) -> Result<{}, {}>;\n", + f.name, f.ret, f.err_ty + )); + } } s.push_str("}\n\n"); @@ -346,54 +349,62 @@ 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" }; l.push(format!("impl Dispatch for {proto}Dispatcher {{")); 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(" out: &mut dyn BufMut,".into()); + l.push(format!(" {out_param}: &mut dyn BufMut,")); l.push(" ) -> Result<(), RuntimeError> {".into()); l.push(format!( " match call.resolve({calls_const}).ok_or(RuntimeError::UnknownCall)? {{" )); for (i, f) in fns.iter().enumerate() { l.push(format!(" {i} => {{")); - let recv = match &f.params_ty { + let call_args = match &f.params_ty { Some(ty) => { l.push(format!(" let p: {ty} = fmt.decode(params)?;")); - let call_args = f - .args + f.args .iter() .map(|a| format!("p.{}", a.name)) .collect::>() - .join(", "); - format!("self.0.{}({call_args})", f.name) + .join(", ") } None => { l.push(" let _: () = fmt.decode(params)?;".into()); - format!("self.0.{}()", f.name) + String::new() } }; - l.push(format!(" match {recv} {{")); - l.push(" Ok(reply) => {".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(" }".into()); - for (ord, err) in &f.throws { - l.push(format!(" Err({}::{err}(e)) => {{", f.err_ty)); + 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. + l.push(format!(" {call};")); + } else { + l.push(format!(" match {call} {{")); + l.push(" Ok(reply) => {".into()); 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(" fmt.encode(&reply, &mut body)?;".into()); + l.push(" Envelope::encode_ok(&body, out);".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(" }".into()); + } + if f.throws.is_empty() { + l.push(" Err(never) => match never {},".into()); + } + l.push(" }".into()); } - if f.throws.is_empty() { - l.push(" Err(never) => match never {},".into()); - } - l.push(" }".into()); l.push(" Ok(())".into()); l.push(" }".into()); } @@ -416,17 +427,7 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap l.push(" Self(client)".into()); l.push(" }".into()); for (i, f) in fns.iter().enumerate() { - let sig_args = f - .args - .iter() - .map(|a| format!("{}: {}", a.name, a.sig_ty)) - .collect::>() - .join(", "); - let sig_args = if sig_args.is_empty() { - String::new() - } else { - format!(", {sig_args}") - }; + let args = sig_args(&f.args); let param_expr = match &f.params_ty { Some(ty) => { let init = f @@ -440,8 +441,18 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap None => "&()".to_string(), }; l.push(String::new()); + if f.one_way { + // Fire-and-forget: no reply, so no `CallError`. + l.push(format!( + " pub fn {}(&mut self{args}) -> Result<(), RuntimeError> {{", + f.name + )); + l.push(format!(" self.0.notify({i}u16, {param_expr})")); + l.push(" }".into()); + continue; + } l.push(format!( - " pub fn {}(&mut self{sig_args}) -> Result<{}, CallError<{}>> {{", + " pub fn {}(&mut self{args}) -> Result<{}, CallError<{}>> {{", f.name, f.ret, f.err_ty )); l.push(format!( @@ -502,10 +513,13 @@ fn fn_info( } else { Some(format!("{proto}{pascal_fn}Params")) }; + let one_way = ret.is_none(); let ret = match ret { None | Some(KindValue::Unit) => "()".to_string(), Some(kv) => rust_type(kv), }; + // A one-way function carries no `!` (nowhere to deliver it); ignore any. + let throws: &[u16] = if one_way { &[] } else { throws }; let throws: Vec<(u16, String)> = throws .iter() .map(|ord| { @@ -526,6 +540,7 @@ fn fn_info( ret, err_ty: format!("{proto}{pascal_fn}Error"), throws, + one_way, } } @@ -543,6 +558,20 @@ fn arg_types(kind: &KindValue) -> (String, String) { // ── name / type helpers ──────────────────────────────────────────────────── +/// The `, name: ty, name: ty` suffix for a method taking `args` after `&self` +/// / `&mut self` — empty when there are none. +fn sig_args(args: &[Arg]) -> String { + if args.is_empty() { + return String::new(); + } + let list = args + .iter() + .map(|a| format!("{}: {}", a.name, a.sig_ty)) + .collect::>() + .join(", "); + format!(", {list}") +} + fn pascal(s: &str) -> String { s.split('_') .filter(|p| !p.is_empty()) diff --git a/codegen/tests/generate.rs b/codegen/tests/generate.rs index d0feaea..90afed7 100644 --- a/codegen/tests/generate.rs +++ b/codegen/tests/generate.rs @@ -147,6 +147,7 @@ fn protocol_errors_map_to_ordinals_and_a_union() { parameters: vec![], name: "Chat".to_string(), functions: vec![ + // request/response with an empty ack (`-> ()`), and a `!` FrozenUnit::Function { docstring: String::new(), name: "send".to_string(), @@ -156,7 +157,7 @@ fn protocol_errors_map_to_ordinals_and_a_union() { kind: KindValue::Namespaced("string".to_string(), None), span: (0, 0), }], - _return: None, // one-way today: empty ack + _return: Some(KindValue::Unit), throws: vec![0], span: (0, 0), }, @@ -169,6 +170,20 @@ fn protocol_errors_map_to_ordinals_and_a_union() { throws: vec![], span: (0, 0), }, + // one-way: no `->` at all + FrozenUnit::Function { + docstring: String::new(), + name: "poke".to_string(), + parameters: vec![], + arguments: vec![FrozenArgument { + name: "note".to_string(), + kind: KindValue::Namespaced("string".to_string(), None), + span: (0, 0), + }], + _return: None, + throws: vec![], // a `!` on a one-way fn is dropped + span: (0, 0), + }, ], span: (0, 0), }; @@ -180,7 +195,7 @@ fn protocol_errors_map_to_ordinals_and_a_union() { assert!(src.contains("pub struct Rejected {")); // per-function enum from the throw ordinal assert!(src.contains("pub enum ChatSendError {\n Rejected(Rejected),\n}")); - // a non-throwing function still gets an (empty) enum + // a non-throwing request/response function still gets an (empty) enum assert!(src.contains("pub enum ChatPingError {\n}")); // per-protocol union + From impl assert!(src.contains("pub enum ChatError {\n Rejected(Rejected),\n}")); @@ -189,15 +204,46 @@ fn protocol_errors_map_to_ordinals_and_a_union() { assert!(src.contains("Envelope::encode_err(0u16, &body, out);")); // client maps that ordinal back assert!(src.contains("Envelope::Err { id: 0u16, body } =>")); - // zero-arg call sends `&()` - assert!(src.contains("self.0.call(1u16, &())")); - // unit / one-way returns render as `()` + // `-> ()` (Unit) is request/response with an empty ack assert!(src.contains("fn ping(&self) -> Result<(), ChatPingError>;")); - // a `str` arg is borrowed: `&str` in the signature, `&'de str` in the - // params struct (decoded borrowed from the receive buffer) + assert!(src.contains("self.0.call(1u16, &())")); + // 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}")); assert!(src.contains("pub fn send(&mut self, body: &str) -> Result<(), CallError>")); + + // one-way `poke`: no error enum, a plain trait method, a `notify` client + 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 })")); +} + +#[test] +fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() { + let proto = FrozenUnit::Protocol { + docstring: "Bus".to_string(), + parameters: vec![], + name: "Bus".to_string(), + functions: vec![FrozenUnit::Function { + docstring: String::new(), + name: "emit".to_string(), + parameters: vec![], + arguments: vec![FrozenArgument { + name: "topic".to_string(), + kind: KindValue::Namespaced("string".to_string(), None), + span: (0, 0), + }], + _return: None, + throws: vec![], + span: (0, 0), + }], + span: (0, 0), + }; + 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,")); } #[test]