From 282dad335ed4784d39d536cefbdbbe8c1b876794 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Tue, 1 Sep 2026 23:22:32 +0800 Subject: [PATCH] feat(ir): drop Function.synchronous, add Function.parameters, KindValue::Unit Three of the four core-target-contract IR changes (docs Design 4.4). The fourth -- throws: Vec -> Vec with schema-global error ordinals -- is its own follow-up. - Function.synchronous removed. It conflated a wire fact (does the call reply), a binding choice (block vs .await) and server config; only the first belongs in the schema and _return already carries it. It was always frozen `true` and read nowhere. - Function.parameters: Vec added, same shape Protocol / Struct already carry -- `@key = value` function annotations (@timeout_ms, @idempotent, ...) as Property { name, expression }. The AST already parsed function annotations; they were dropped at freeze. Open namespace: a consumer acts on the keys it knows. - KindValue::Unit + a `()` type in the grammar. `function commit() -> ();` now freezes as _return: Some(KindValue::Unit) -- a reply that carries no value (an empty ack) -- as distinct from `function commit();`, no `->` at all, which freezes as _return: None (no reply, one-way). Without the variant "ack, no value" was inexpressible. Exhaustive matches updated in diff.rs, validator.rs, kind_search.rs; type_to_string / build_kind_value map the new grammar node. Tests: parser + IR coverage for `-> ()` vs no return. Full core suite green (241 passed). --- core/src/schema/idl/grammar.rs | 11 +++++ .../ir/compiler/interpreted/kind_search.rs | 6 ++- .../ir/compiler/interpreter/incremental.rs | 7 +++- core/src/schema/ir/diff.rs | 1 + core/src/schema/ir/frozen/unit.rs | 6 ++- core/src/schema/ir/validation/validator.rs | 3 ++ core/tests/schema/ir/generation.rs | 42 +++++++++++++++++++ core/tests/schema/parser/comprehensive.rs | 7 ++++ 8 files changed, 80 insertions(+), 3 deletions(-) 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; }";