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:
135
src/types.rs
135
src/types.rs
@@ -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))?;
|
||||
|
||||
Reference in New Issue
Block a user