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

@@ -25,11 +25,12 @@ pub enum Type {
String,
Char,
Unit,
/// Function type with effects
/// Function type with effects and behavioral properties
Function {
params: Vec<Type>,
return_type: Box<Type>,
effects: EffectSet,
properties: PropertySet,
},
/// Generic type application: List<Int>, Option<String>
App {
@@ -72,6 +73,7 @@ impl Type {
params,
return_type: Box::new(return_type),
effects: EffectSet::empty(),
properties: PropertySet::empty(),
}
}
@@ -80,6 +82,21 @@ impl Type {
params,
return_type: Box::new(return_type),
effects,
properties: PropertySet::empty(),
}
}
pub fn function_with_properties(
params: Vec<Type>,
return_type: Type,
effects: EffectSet,
properties: PropertySet,
) -> Self {
Type::Function {
params,
return_type: Box::new(return_type),
effects,
properties,
}
}
@@ -121,10 +138,12 @@ impl Type {
params,
return_type,
effects,
properties,
} => Type::Function {
params: params.iter().map(|p| p.apply(subst)).collect(),
return_type: Box::new(return_type.apply(subst)),
effects: effects.clone(),
properties: properties.clone(),
},
Type::App { constructor, args } => Type::App {
constructor: Box::new(constructor.apply(subst)),
@@ -209,6 +228,7 @@ impl fmt::Display for Type {
params,
return_type,
effects,
properties,
} => {
write!(f, "fn(")?;
for (i, p) in params.iter().enumerate() {
@@ -221,6 +241,9 @@ impl fmt::Display for Type {
if !effects.is_empty() {
write!(f, " with {{{}}}", effects)?;
}
if !properties.is_empty() {
write!(f, " {}", properties)?;
}
Ok(())
}
Type::App { constructor, args } => {
@@ -335,6 +358,109 @@ impl fmt::Display for EffectSet {
}
}
/// Set of behavioral properties for a function
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PropertySet {
pub properties: HashSet<Property>,
}
/// A behavioral property (mirrors BehavioralProperty from AST but for type system)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Property {
Pure,
Total,
Idempotent,
Deterministic,
Commutative,
}
impl From<crate::ast::BehavioralProperty> for Property {
fn from(p: crate::ast::BehavioralProperty) -> Self {
match p {
crate::ast::BehavioralProperty::Pure => Property::Pure,
crate::ast::BehavioralProperty::Total => Property::Total,
crate::ast::BehavioralProperty::Idempotent => Property::Idempotent,
crate::ast::BehavioralProperty::Deterministic => Property::Deterministic,
crate::ast::BehavioralProperty::Commutative => Property::Commutative,
}
}
}
impl fmt::Display for Property {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Property::Pure => write!(f, "pure"),
Property::Total => write!(f, "total"),
Property::Idempotent => write!(f, "idempotent"),
Property::Deterministic => write!(f, "deterministic"),
Property::Commutative => write!(f, "commutative"),
}
}
}
impl PropertySet {
pub fn empty() -> Self {
Self {
properties: HashSet::new(),
}
}
pub fn from_ast(props: &[crate::ast::BehavioralProperty]) -> Self {
Self {
properties: props.iter().copied().map(Property::from).collect(),
}
}
pub fn is_empty(&self) -> bool {
self.properties.is_empty()
}
pub fn contains(&self, prop: Property) -> bool {
self.properties.contains(&prop)
}
pub fn is_pure(&self) -> bool {
self.contains(Property::Pure)
}
pub fn is_total(&self) -> bool {
self.contains(Property::Total)
}
pub fn insert(&mut self, prop: Property) {
self.properties.insert(prop);
}
/// Check if this property set is a superset of another (satisfies constraints)
pub fn satisfies(&self, required: &PropertySet) -> bool {
required.properties.is_subset(&self.properties)
}
/// Intersection of two property sets (for composition)
pub fn intersection(&self, other: &PropertySet) -> PropertySet {
PropertySet {
properties: self
.properties
.intersection(&other.properties)
.copied()
.collect(),
}
}
}
impl fmt::Display for PropertySet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let props: Vec<_> = self.properties.iter().collect();
for (i, p) in props.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "is {}", p)?;
}
Ok(())
}
}
/// Type substitution (mapping from type variables to types)
#[derive(Debug, Clone, Default)]
pub struct Substitution {
@@ -941,11 +1067,13 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
params: p1,
return_type: r1,
effects: e1,
properties: props1,
},
Type::Function {
params: p2,
return_type: r2,
effects: e2,
properties: props2,
},
) => {
if p1.len() != p2.len() {
@@ -964,6 +1092,11 @@ pub fn unify(t1: &Type, t2: &Type) -> Result<Substitution, String> {
));
}
// Properties are not checked during unification - they are guarantees, not constraints
// A pure function can be used anywhere a function is expected
// Property requirements are enforced via where clauses, not type unification
let _ = (props1, props2); // Acknowledge but don't enforce
let mut subst = Substitution::new();
for (a, b) in p1.iter().zip(p2.iter()) {
let s = unify(&a.apply(&subst), &b.apply(&subst))?;