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:
303
src/parser.rs
303
src/parser.rs
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user