Add behavioral types system

Implement behavioral properties for functions including:
- Property annotations: is pure, is total, is idempotent, is deterministic, is commutative
- Where clause constraints: where F is pure
- Result refinements: where result >= 0 (parsing only, not enforced)

Key changes:
- AST: BehavioralProperty enum, WhereClause enum, updated FunctionDecl
- Lexer: Added keywords (is, pure, total, idempotent, deterministic, commutative, where, assume)
- Parser: parse_behavioral_properties(), parse_where_clauses(), parse_single_property()
- Types: PropertySet for tracking function properties, updated Function type
- Typechecker: Verify pure functions don't have effects, validate where clause type params

Properties are informational/guarantees rather than type constraints - a pure
function can be used anywhere a function is expected. Property requirements
are meant to be enforced via where clauses (future work: call-site checking).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 03:30:51 -05:00
parent 15e5ccb064
commit 66132779cc
6 changed files with 624 additions and 4 deletions

View File

@@ -191,6 +191,12 @@ impl Parser {
Vec::new()
};
// Optional behavioral properties: is pure, is total, etc.
let properties = self.parse_behavioral_properties()?;
// Optional where clauses
let where_clauses = self.parse_where_clauses()?;
self.expect(TokenKind::Eq)?;
self.skip_newlines();
let body = self.parse_expr()?;
@@ -203,6 +209,8 @@ impl Parser {
params,
return_type,
effects,
properties,
where_clauses,
body,
span,
})
@@ -490,6 +498,171 @@ impl Parser {
Ok(effects)
}
/// Parse behavioral properties: is pure, is total, is idempotent, etc.
fn parse_behavioral_properties(&mut self) -> Result<Vec<BehavioralProperty>, ParseError> {
let mut properties = Vec::new();
while self.check(TokenKind::Is) || self.check(TokenKind::Assume) {
let is_assumed = self.check(TokenKind::Assume);
self.advance(); // consume 'is' or 'assume'
if is_assumed {
// 'assume' must be followed by 'is'
if !self.check(TokenKind::Is) {
return Err(ParseError {
message: "Expected 'is' after 'assume'".to_string(),
span: self.current_span(),
});
}
self.advance(); // consume 'is'
}
let property = self.parse_single_property()?;
properties.push(property);
// Optional comma for multiple properties: is pure, is total
if self.check(TokenKind::Comma) {
self.advance();
}
}
Ok(properties)
}
/// Parse a single behavioral property keyword
fn parse_single_property(&mut self) -> Result<BehavioralProperty, ParseError> {
let span = self.current_span();
match self.peek_kind() {
TokenKind::Pure => {
self.advance();
Ok(BehavioralProperty::Pure)
}
TokenKind::Total => {
self.advance();
Ok(BehavioralProperty::Total)
}
TokenKind::Idempotent => {
self.advance();
Ok(BehavioralProperty::Idempotent)
}
TokenKind::Deterministic => {
self.advance();
Ok(BehavioralProperty::Deterministic)
}
TokenKind::Commutative => {
self.advance();
Ok(BehavioralProperty::Commutative)
}
_ => Err(ParseError {
message: "Expected behavioral property: pure, total, idempotent, deterministic, or commutative".to_string(),
span,
}),
}
}
/// Parse where clauses: where F is pure, where result > 0
fn parse_where_clauses(&mut self) -> Result<Vec<WhereClause>, ParseError> {
let mut clauses = Vec::new();
while self.check(TokenKind::Where) {
self.advance(); // consume 'where'
let span = self.current_span();
// Check if it's a property constraint: where F is pure
// or a result refinement: where result > 0
if self.check_ident() {
let ident = self.parse_ident()?;
if self.check(TokenKind::Is) {
self.advance(); // consume 'is'
let property = self.parse_single_property()?;
clauses.push(WhereClause::PropertyConstraint {
type_param: ident,
property,
span,
});
} else {
// This is a result refinement starting with an identifier
// For now, we'll parse it as a simple expression
// Put the identifier back by creating an expression
let predicate = self.parse_refinement_with_ident(ident)?;
clauses.push(WhereClause::ResultRefinement {
predicate: Box::new(predicate),
span,
});
}
} else {
return Err(ParseError {
message: "Expected identifier after 'where'".to_string(),
span,
});
}
// Optional comma for multiple where clauses
if self.check(TokenKind::Comma) {
self.advance();
}
}
Ok(clauses)
}
/// Parse a refinement expression that starts with an already-parsed identifier
fn parse_refinement_with_ident(&mut self, ident: Ident) -> Result<Expr, ParseError> {
// Start with the identifier as a variable
let mut left = Expr::Var(ident);
// Parse the rest as a comparison expression
if let Some(op) = self.try_parse_comparison_op() {
let right = self.parse_primary_expr()?;
let span = left.span().merge(right.span());
left = Expr::BinaryOp {
op,
left: Box::new(left),
right: Box::new(right),
span,
};
}
Ok(left)
}
/// Try to parse a comparison operator
fn try_parse_comparison_op(&mut self) -> Option<BinaryOp> {
match self.peek_kind() {
TokenKind::Lt => {
self.advance();
Some(BinaryOp::Lt)
}
TokenKind::Le => {
self.advance();
Some(BinaryOp::Le)
}
TokenKind::Gt => {
self.advance();
Some(BinaryOp::Gt)
}
TokenKind::Ge => {
self.advance();
Some(BinaryOp::Ge)
}
TokenKind::EqEq => {
self.advance();
Some(BinaryOp::Eq)
}
TokenKind::Ne => {
self.advance();
Some(BinaryOp::Ne)
}
_ => None,
}
}
/// Check if the current token is an identifier
fn check_ident(&self) -> bool {
matches!(self.peek_kind(), TokenKind::Ident(_))
}
/// Parse a type expression
fn parse_type(&mut self) -> Result<TypeExpr, ParseError> {
// Function type: fn(A, B): C with {E}
@@ -1932,4 +2105,134 @@ mod tests {
panic!("Expected type declaration");
}
}
// ============ Behavioral Properties Tests ============
#[test]
fn test_parse_function_is_pure() {
let source = "fn add(a: Int, b: Int): Int is pure = a + b";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.name.name, "add");
assert_eq!(f.properties.len(), 1);
assert_eq!(f.properties[0], BehavioralProperty::Pure);
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_function_multiple_properties() {
let source = "fn double(x: Int): Int is pure, is total = x * 2";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.name.name, "double");
assert_eq!(f.properties.len(), 2);
assert!(f.properties.contains(&BehavioralProperty::Pure));
assert!(f.properties.contains(&BehavioralProperty::Total));
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_function_with_effects_and_properties() {
let source = "fn log(msg: String): Unit with {Logger} is deterministic = Logger.log(msg)";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.name.name, "log");
assert_eq!(f.effects.len(), 1);
assert_eq!(f.effects[0].name, "Logger");
assert_eq!(f.properties.len(), 1);
assert_eq!(f.properties[0], BehavioralProperty::Deterministic);
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_function_idempotent() {
let source = "fn normalize(s: String): String is idempotent = s";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.properties.len(), 1);
assert_eq!(f.properties[0], BehavioralProperty::Idempotent);
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_function_commutative() {
let source = "fn add(a: Int, b: Int): Int is commutative = a + b";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.properties.len(), 1);
assert_eq!(f.properties[0], BehavioralProperty::Commutative);
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_where_property_constraint() {
let source = "fn retry(action: F): Int where F is idempotent = 0";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.name.name, "retry");
assert_eq!(f.where_clauses.len(), 1);
if let WhereClause::PropertyConstraint {
type_param,
property,
..
} = &f.where_clauses[0]
{
assert_eq!(type_param.name, "F");
assert_eq!(*property, BehavioralProperty::Idempotent);
} else {
panic!("Expected PropertyConstraint");
}
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_where_result_refinement() {
let source = "fn abs(x: Int): Int where result >= 0 = if x < 0 then 0 - x else x";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(f) = &program.declarations[0] {
assert_eq!(f.name.name, "abs");
assert_eq!(f.where_clauses.len(), 1);
if let WhereClause::ResultRefinement { predicate, .. } = &f.where_clauses[0] {
// Check that the predicate is a binary comparison
if let Expr::BinaryOp { op, .. } = predicate.as_ref() {
assert_eq!(*op, BinaryOp::Ge);
} else {
panic!("Expected BinaryOp in refinement");
}
} else {
panic!("Expected ResultRefinement");
}
} else {
panic!("Expected function declaration");
}
}
#[test]
fn test_parse_all_behavioral_features() {
// Single-line version to avoid newline issues
let source = "fn process(f: F, x: Int): Int with {Logger} is pure, is total where F is deterministic = f(x)";
let program = Parser::parse_source(source).unwrap();
if let Declaration::Function(func) = &program.declarations[0] {
assert_eq!(func.name.name, "process");
assert_eq!(func.effects.len(), 1);
assert_eq!(func.properties.len(), 2);
assert!(func.properties.contains(&BehavioralProperty::Pure));
assert!(func.properties.contains(&BehavioralProperty::Total));
assert_eq!(func.where_clauses.len(), 1);
} else {
panic!("Expected function declaration");
}
}
}