Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions crates/allium-parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2535,6 +2535,18 @@ impl<'s> Parser<'s> {
fn parse_paren_expr(&mut self) -> Option<Expr> {
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();
Expand Down Expand Up @@ -6312,4 +6324,47 @@ rule R {
errors.iter().map(|d| &d.message).collect::<Vec<_>>(),
);
}

#[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<Foo> }",
);
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::<Vec<_>>(),
);

// The empty parens parse to an empty parameter list: the signature
// value is `Set<Foo>` (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:?}",
);
}
}
11 changes: 11 additions & 0 deletions docs/allium-v3-language-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Foo>
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:
Expand Down