diff --git a/crates/allium-parser/src/parser.rs b/crates/allium-parser/src/parser.rs index 4e3f868..dad2c18 100644 --- a/crates/allium-parser/src/parser.rs +++ b/crates/allium-parser/src/parser.rs @@ -2535,6 +2535,18 @@ impl<'s> Parser<'s> { fn parse_paren_expr(&mut self) -> Option { let start = self.advance().span; // ( + // Empty parameter list `()` — e.g. a zero-argument contract signature + // `op: () -> ReturnType`. Represented as an empty block so the + // trailing `-> Type` projection attaches the return type with no + // parameters, mirroring how multi-parameter lists become a block. + if self.at(TokenKind::RParen) { + let end = self.advance().span; // ) + return Some(Expr::Block { + span: start.merge(end), + items: Vec::new(), + }); + } + // Detect `(name: expr, ...)` — typed signature parameters. if self.peek_kind().is_word() && self.peek_at(1).kind == TokenKind::Colon { let mut bindings = Vec::new(); @@ -6312,4 +6324,47 @@ rule R { errors.iter().map(|d| &d.message).collect::>(), ); } + + #[test] + fn zero_arg_contract_signature_parens() { + // `op: () -> ReturnType` declares a zero-argument contract operation. + let r = parse_ok( + "-- allium: 3\nvalue Foo { value: String }\n\ + contract Demo { list_things: () -> Set }", + ); + assert!( + r.diagnostics.iter().all(|d| d.severity != Severity::Error), + "zero-arg signature should parse without errors, got: {:?}", + r.diagnostics + .iter() + .map(|d| &d.message) + .collect::>(), + ); + + // The empty parens parse to an empty parameter list: the signature + // value is `Set` (a GenericType) whose name is a `() -> Set` + // projection with an empty block as its source. + let contract = r + .module + .declarations + .iter() + .find_map(|d| match d { + Decl::Block(b) if b.kind == BlockKind::Contract => Some(b), + _ => None, + }) + .expect("contract decl"); + let BlockItemKind::Assignment { value, .. } = &contract.items[0].kind else { + panic!("expected signature assignment, got {:?}", contract.items[0].kind); + }; + let Expr::GenericType { name, .. } = value else { + panic!("expected GenericType signature value, got {value:?}"); + }; + let Expr::ProjectionMap { source, .. } = name.as_ref() else { + panic!("expected `params -> Return` projection, got {name:?}"); + }; + assert!( + matches!(source.as_ref(), Expr::Block { items, .. } if items.is_empty()), + "zero-arg params should be an empty block, got {source:?}", + ); + } } diff --git a/docs/allium-v3-language-reference.md b/docs/allium-v3-language-reference.md index 57fb911..ef74ce9 100644 --- a/docs/allium-v3-language-reference.md +++ b/docs/allium-v3-language-reference.md @@ -150,6 +150,17 @@ contract Codec { Contract bodies contain typed signatures and annotations (`@invariant`, `@guidance`). Entity, value, enum and variant declarations are prohibited inside contracts. Types referenced in signatures must be declared at module level or imported via `use`. +A zero-argument operation uses an empty parameter list: + +``` +contract Registry { + list_things: () -> Set + health: () -> Status +} +``` + +`name: () -> ReturnType` is the only form for parameterless operations; a bare arrow (`name: -> ReturnType`) is not valid. + ### Referencing contracts in surfaces Surfaces reference contracts in a `contracts:` clause. Each entry uses `demands` or `fulfils` to indicate the direction of the obligation: