diff --git a/codegen/src/generator.rs b/codegen/src/generator.rs index a465fb5..baf7cc8 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 = "435b7e833f970fadf4edbedcade20037143713d5"; +const RUNTIME_REV: &str = "0501812f57817d7b11165bc6a24f54afa45acb16"; pub fn generate_rust(req: &GenRequest) -> Result> { match req.mode { @@ -240,6 +240,9 @@ struct FnInfo { /// explicit `-> ()` (`KindValue::Unit`) is *not* this — it's a normal /// request/response with an empty ack (§4.4). one_way: bool, + /// `@timeout_ms = N` on the function — the client method calls + /// `Client::call_with_timeout` with `Duration::from_millis(N)`. + timeout_ms: Option, } fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap) -> String { @@ -248,11 +251,14 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap .filter_map(|f| match f { FrozenUnit::Function { name, + parameters, arguments, _return, throws, .. - } => Some(fn_info(proto, name, arguments, _return, throws, errors)), + } => Some(fn_info( + proto, name, parameters, arguments, _return, throws, errors, + )), _ => None, }) .collect(); @@ -455,9 +461,13 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap " pub fn {}(&mut self{args}) -> Result<{}, CallError<{}>> {{", f.name, f.ret, f.err_ty )); - l.push(format!( - " let (reply, fmt) = self.0.call({i}u16, {param_expr})?;" - )); + let call = match f.timeout_ms { + Some(ms) => format!( + "self.0.call_with_timeout({i}u16, {param_expr}, core::time::Duration::from_millis({ms}))" + ), + None => format!("self.0.call({i}u16, {param_expr})"), + }; + l.push(format!(" let (reply, fmt) = {call}?;")); l.push(" match reply {".into()); l.push( " Envelope::Ok(payload) => fmt.decode(payload).map_err(CallError::Runtime)," @@ -490,12 +500,14 @@ fn protocol(proto: &str, functions: &[FrozenUnit], errors: &HashMap fn fn_info( proto: &str, name: &str, + parameters: &[FrozenUnit], arguments: &[FrozenArgument], ret: &Option, throws: &[u16], errors: &HashMap, ) -> FnInfo { let pascal_fn = pascal(name); + let timeout_ms = annotation(parameters, "timeout_ms").and_then(|v| v.parse::().ok()); let args: Vec = arguments .iter() .map(|a| { @@ -541,9 +553,19 @@ fn fn_info( err_ty: format!("{proto}{pascal_fn}Error"), throws, one_way, + timeout_ms, } } +/// The value of a scalar `@key = value` annotation (frozen as +/// `FrozenUnit::Property`), by key. +fn annotation<'a>(parameters: &'a [FrozenUnit], key: &str) -> Option<&'a str> { + parameters.iter().find_map(|p| match p { + FrozenUnit::Property { name, expression } if name == key => expression.as_deref(), + _ => None, + }) +} + /// `(signature type, params-struct field type)` for a function argument. /// A `str` / `string` arg is passed by reference and decoded borrowed from /// the receive buffer; everything else is owned (borrowed arrays / nested diff --git a/codegen/tests/compiles.rs b/codegen/tests/compiles.rs index c4466e5..5e3800b 100644 --- a/codegen/tests/compiles.rs +++ b/codegen/tests/compiles.rs @@ -101,6 +101,19 @@ fn chat_schema() -> Vec { ), function("wipe", vec![], Some(KindValue::Unit), vec![]), function("poke", vec![], None, vec![]), + // @timeout_ms = 3000 → the client emits `call_with_timeout` + FrozenUnit::Function { + docstring: String::new(), + parameters: vec![FrozenUnit::Property { + name: "timeout_ms".into(), + expression: Some("3000".into()), + }], + name: "await_ack".into(), + arguments: vec![arg("token", KindValue::Namespaced("string".into(), None))], + _return: Some(KindValue::Unit), + throws: vec![], + span: (0, 0), + }, ], span: (0, 0), }, diff --git a/codegen/tests/generate.rs b/codegen/tests/generate.rs index 90afed7..8157547 100644 --- a/codegen/tests/generate.rs +++ b/codegen/tests/generate.rs @@ -219,6 +219,48 @@ fn protocol_errors_map_to_ordinals_and_a_union() { assert!(src.contains("self.0.notify(2u16, &ChatPokeParams { note })")); } +#[test] +fn timeout_ms_annotation_emits_call_with_timeout() { + let proto = FrozenUnit::Protocol { + docstring: "Api".to_string(), + parameters: vec![], + name: "Api".to_string(), + functions: vec![ + // @timeout_ms = 2500 + FrozenUnit::Function { + docstring: String::new(), + name: "slow".to_string(), + parameters: vec![FrozenUnit::Property { + name: "timeout_ms".to_string(), + expression: Some("2500".to_string()), + }], + arguments: vec![], + _return: Some(KindValue::Primitive(Primitive::U32(None))), + throws: vec![], + span: (0, 0), + }, + // no annotation + FrozenUnit::Function { + docstring: String::new(), + name: "fast".to_string(), + parameters: vec![], + arguments: vec![], + _return: Some(KindValue::Primitive(Primitive::U32(None))), + throws: vec![], + span: (0, 0), + }, + ], + span: (0, 0), + }; + let schemas = vec![("api".to_string(), vec![proto])]; + 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))?;" + )); + assert!(src.contains("self.0.call(1u16, &())?;")); +} + #[test] fn an_all_one_way_protocol_leaves_the_dispatch_out_param_unbound() { let proto = FrozenUnit::Protocol {