diff --git a/core/src/schema/idl/grammar.rs b/core/src/schema/idl/grammar.rs index c6f0c09..461c3d7 100644 --- a/core/src/schema/idl/grammar.rs +++ b/core/src/schema/idl/grammar.rs @@ -710,6 +710,17 @@ pub mod grammar { Named(ScopedIdentifier), Array(Box), Union(UnionType), + Unit(UnitType), + } + + /// The unit type `()` - an explicit "reply, but no value" (an empty ack), + /// distinct from omitting `->` entirely, which means "no reply at all". + #[derive(Debug, Clone)] + pub struct UnitType { + #[rust_sitter::leaf(text = "(")] + _open: (), + #[rust_sitter::leaf(text = ")")] + _close: (), } /// Array type: Type[] or Type[SIZE] diff --git a/core/src/schema/ir/compiler/interpreted/kind_search.rs b/core/src/schema/ir/compiler/interpreted/kind_search.rs index 0d66e95..b98b62a 100644 --- a/core/src/schema/ir/compiler/interpreted/kind_search.rs +++ b/core/src/schema/ir/compiler/interpreted/kind_search.rs @@ -99,7 +99,10 @@ pub enum KindValue { Primitive(Primitive), EnumVariant(String, Option>), Union(Vec), - Namespaced(String, Option>) + Namespaced(String, Option>), + /// The unit type `()` - a reply carrying no value. Distinct from a + /// function with no `_return` at all (no reply). + Unit, } impl KindValue { @@ -127,6 +130,7 @@ impl KindValue { // TODO: Properly implement, it was testing at this stage (namespace.clone(), None) } + KindValue::Unit => ("()".to_owned(), None), } } } diff --git a/core/src/schema/ir/compiler/interpreter/incremental.rs b/core/src/schema/ir/compiler/interpreter/incremental.rs index a9b074a..48f3e91 100644 --- a/core/src/schema/ir/compiler/interpreter/incremental.rs +++ b/core/src/schema/ir/compiler/interpreter/incremental.rs @@ -183,9 +183,9 @@ impl IncrementalInterpreter { FrozenUnit::Function { name: func_name, + parameters: annotation_units(&func.annotations()), arguments, _return: return_type, - synchronous: true, docstring: func.docstring().unwrap_or_default(), throws: func.throws().into_iter().collect(), span: func.span, @@ -371,6 +371,10 @@ fn build_kind_value( ); } + if matches!(type_def, crate::schema::idl::grammar::Type::Unit(_)) { + return KindValue::Unit; + } + let type_name = type_to_string(type_def); match (type_name.as_str(), value) { @@ -415,6 +419,7 @@ fn type_to_string(type_def: &crate::schema::idl::grammar::Type) -> String { let members: Vec = union_type.members().iter().map(type_to_string).collect(); format!("union({})", members.join(" ")) } + crate::schema::idl::grammar::Type::Unit(_) => "()".to_string(), } } diff --git a/core/src/schema/ir/diff.rs b/core/src/schema/ir/diff.rs index 018b81f..3816c0b 100644 --- a/core/src/schema/ir/diff.rs +++ b/core/src/schema/ir/diff.rs @@ -544,6 +544,7 @@ fn kind_to_string(kind: &KindValue) -> String { let parts: Vec = members.iter().map(kind_to_string).collect(); format!("union({})", parts.join(" | ")) } + KindValue::Unit => "()".to_string(), } } diff --git a/core/src/schema/ir/frozen/unit.rs b/core/src/schema/ir/frozen/unit.rs index e67457f..f8cd052 100644 --- a/core/src/schema/ir/frozen/unit.rs +++ b/core/src/schema/ir/frozen/unit.rs @@ -81,8 +81,12 @@ pub enum FrozenUnit { }, Function { docstring: String, + // `@key = value` function annotations (per-call settings: `@timeout_ms`, + // `@idempotent`, ...) as `Property { name, expression }`, same shape as + // `Protocol` / `Struct` carry. Open namespace - a consumer acts on the + // keys it knows and ignores the rest. + parameters: Vec, name: String, - synchronous: bool, // direction: Box, arguments: Vec, _return: Option, diff --git a/core/src/schema/ir/validation/validator.rs b/core/src/schema/ir/validation/validator.rs index 890472d..7af1175 100644 --- a/core/src/schema/ir/validation/validator.rs +++ b/core/src/schema/ir/validation/validator.rs @@ -401,6 +401,9 @@ fn validate_type( // type today (it's only ever used to represent one variant inside // an Enum's own `variants` list, not as a referenceable type). } + KindValue::Unit => { + // `()` - the empty-reply type. Nothing to resolve. + } } } diff --git a/core/tests/schema/ir/generation.rs b/core/tests/schema/ir/generation.rs index 58ad53a..2f7e732 100644 --- a/core/tests/schema/ir/generation.rs +++ b/core/tests/schema/ir/generation.rs @@ -403,6 +403,48 @@ protocol EventService { } } + #[test] + fn test_protocol_unit_return_ir() { + // `-> ()` freezes as `_return: Some(KindValue::Unit)` - an empty + // reply - where a function with no `->` freezes as `_return: None`. + use comline_core::schema::ir::compiler::interpreted::kind_search::KindValue; + + let code = r#" +protocol TxService { + function commit() -> (); + function log(str); +} +"#; + let ir_units = IncrementalInterpreter::from_source(code); + match &ir_units[0] { + comline_core::schema::ir::frozen::unit::FrozenUnit::Protocol { functions, .. } => { + match &functions[0] { + comline_core::schema::ir::frozen::unit::FrozenUnit::Function { + name, + _return, + .. + } => { + assert_eq!(name, "commit"); + assert_eq!(_return, &Some(KindValue::Unit)); + } + _ => panic!("Expected Function"), + } + match &functions[1] { + comline_core::schema::ir::frozen::unit::FrozenUnit::Function { + name, + _return, + .. + } => { + assert_eq!(name, "log"); + assert_eq!(_return, &None); + } + _ => panic!("Expected Function"), + } + } + _ => panic!("Expected Protocol"), + } + } + #[test] fn test_struct_and_field_spans_are_populated() { let code = "struct User {\n id: u64\n name: str\n}\n"; diff --git a/core/tests/schema/parser/comprehensive.rs b/core/tests/schema/parser/comprehensive.rs index 2515615..8031800 100644 --- a/core/tests/schema/parser/comprehensive.rs +++ b/core/tests/schema/parser/comprehensive.rs @@ -363,6 +363,13 @@ enum DayOfWeek { assert!(grammar::parse(code).is_ok()); } + #[test] + fn test_protocol_unit_return() { + // `-> ()` is an explicit empty reply, distinct from no `->` at all. + let code = "protocol API { function commit() -> (); }"; + assert!(grammar::parse(code).is_ok()); + } + #[test] fn test_protocol_multiple_args() { let code = "protocol API { function process(str, u32, bool) -> s64; }";