feat: add schema evolution type system integration and HTTP server effect
Schema Evolution: - Preserve version info in type resolution (Type::Versioned) - Track versioned type declarations in typechecker - Detect version mismatches at compile time (@v1 vs @v2 errors) - Support @v2+ (at least) and @latest version constraints - Store migrations for future auto-migration support - Fix let bindings to preserve declared type annotations HTTP Server Effect: - Add HttpServer effect with listen, accept, respond, respondWithHeaders, stop - Implement blocking request handling via tiny_http - Request record includes method, path, body, headers - Add http_server.lux example with routing via pattern matching - Add type-checking test for HttpServer effect Tests: 222 passing (up from 217) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,9 @@ use crate::diagnostics::{find_similar_names, format_did_you_mean, Diagnostic, Se
|
||||
use crate::exhaustiveness::{check_exhaustiveness, missing_patterns_hint};
|
||||
use crate::modules::ModuleLoader;
|
||||
use crate::types::{
|
||||
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, PropertySet, TraitBoundDef,
|
||||
TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef, VariantFieldsDef,
|
||||
self, unify, EffectDef, EffectOpDef, EffectSet, HandlerDef, Property, PropertySet,
|
||||
TraitBoundDef, TraitDef, TraitImpl, TraitMethodDef, Type, TypeEnv, TypeScheme, VariantDef,
|
||||
VariantFieldsDef, VersionInfo,
|
||||
};
|
||||
|
||||
/// Type checking error
|
||||
@@ -100,6 +101,388 @@ fn categorize_type_error(message: &str) -> (String, Vec<String>) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an operator is commutative
|
||||
fn is_commutative_op(op: BinaryOp) -> bool {
|
||||
matches!(
|
||||
op,
|
||||
BinaryOp::Add | BinaryOp::Mul | BinaryOp::Eq | BinaryOp::Ne | BinaryOp::And | BinaryOp::Or
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if a function body represents a commutative operation on its two parameters
|
||||
fn is_commutative_body(body: &Expr, param1: &str, param2: &str) -> bool {
|
||||
match body {
|
||||
Expr::BinaryOp { op, left, right, .. } => {
|
||||
if !is_commutative_op(*op) {
|
||||
return false;
|
||||
}
|
||||
// Check if it's (param1 op param2) or (param2 op param1)
|
||||
let left_is_p1 = matches!(left.as_ref(), Expr::Var(id) if id.name == param1);
|
||||
let left_is_p2 = matches!(left.as_ref(), Expr::Var(id) if id.name == param2);
|
||||
let right_is_p1 = matches!(right.as_ref(), Expr::Var(id) if id.name == param1);
|
||||
let right_is_p2 = matches!(right.as_ref(), Expr::Var(id) if id.name == param2);
|
||||
(left_is_p1 && right_is_p2) || (left_is_p2 && right_is_p1)
|
||||
}
|
||||
// Handle block expressions - check last expression
|
||||
Expr::Block { statements, result, .. } => {
|
||||
if !statements.is_empty() {
|
||||
return false;
|
||||
}
|
||||
is_commutative_body(result, param1, param2)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an expression references any of the given parameter names
|
||||
fn references_params(expr: &Expr, params: &[&str]) -> bool {
|
||||
match expr {
|
||||
Expr::Var(id) => params.contains(&id.name.as_str()),
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
references_params(left, params) || references_params(right, params)
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => references_params(operand, params),
|
||||
Expr::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
..
|
||||
} => {
|
||||
references_params(condition, params)
|
||||
|| references_params(then_branch, params)
|
||||
|| references_params(else_branch, params)
|
||||
}
|
||||
Expr::Call { func, args, .. } => {
|
||||
references_params(func, params) || args.iter().any(|a| references_params(a, params))
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
statements.iter().any(|s| match s {
|
||||
Statement::Let { value, .. } => references_params(value, params),
|
||||
Statement::Expr(e) => references_params(e, params),
|
||||
}) || references_params(result, params)
|
||||
}
|
||||
Expr::Field { object, .. } => references_params(object, params),
|
||||
Expr::Lambda { body, .. } => references_params(body, params),
|
||||
Expr::Tuple { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||
Expr::List { elements, .. } => elements.iter().any(|e| references_params(e, params)),
|
||||
Expr::Record { fields, .. } => fields.iter().any(|(_, e)| references_params(e, params)),
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
references_params(scrutinee, params)
|
||||
|| arms.iter().any(|a| references_params(&a.body, params))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a function body is idempotent (f(f(x)) == f(x))
|
||||
/// Uses conservative pattern recognition
|
||||
fn is_idempotent_body(body: &Expr, params: &[Parameter]) -> bool {
|
||||
let param_names: Vec<&str> = params.iter().map(|p| p.name.name.as_str()).collect();
|
||||
|
||||
// Pattern 1: Constant - body doesn't reference any parameters
|
||||
if !references_params(body, ¶m_names) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pattern 2: Identity function (single param, body is just that param)
|
||||
if params.len() == 1 {
|
||||
if let Expr::Var(id) = body {
|
||||
if id.name == params[0].name.name {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Field projection on single param (e.g., person.name)
|
||||
if params.len() == 1 {
|
||||
if let Expr::Field { object, .. } = body {
|
||||
if let Expr::Var(id) = object.as_ref() {
|
||||
if id.name == params[0].name.name {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 4: Clamping pattern - if x < min then min else if x > max then max else x
|
||||
// or simpler: if x < 0 then 0 else x
|
||||
if params.len() == 1 && is_clamping_pattern(body, ¶ms[0].name.name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pattern 5: Absolute value - if x < 0 then -x else x
|
||||
if params.len() == 1 && is_abs_pattern(body, ¶ms[0].name.name) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handle block wrapper
|
||||
if let Expr::Block { statements, result, .. } = body {
|
||||
if statements.is_empty() {
|
||||
return is_idempotent_body(result, params);
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if body matches: if x < bound then bound else x (or similar clamping patterns)
|
||||
fn is_clamping_pattern(body: &Expr, param: &str) -> bool {
|
||||
if let Expr::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
..
|
||||
} = body
|
||||
{
|
||||
// Check if then_branch is a constant and else_branch is the param (or recursive)
|
||||
let else_is_param = matches!(else_branch.as_ref(), Expr::Var(id) if id.name == param);
|
||||
let else_is_clamp = is_clamping_pattern(else_branch, param);
|
||||
|
||||
if else_is_param || else_is_clamp {
|
||||
// Check if condition is a comparison involving the param
|
||||
if let Expr::BinaryOp { left, right, op, .. } = condition.as_ref() {
|
||||
let left_is_param = matches!(left.as_ref(), Expr::Var(id) if id.name == param);
|
||||
let right_is_param = matches!(right.as_ref(), Expr::Var(id) if id.name == param);
|
||||
if (left_is_param || right_is_param)
|
||||
&& matches!(
|
||||
op,
|
||||
BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge
|
||||
)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if body matches: if x < 0 then -x else x
|
||||
fn is_abs_pattern(body: &Expr, param: &str) -> bool {
|
||||
if let Expr::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
..
|
||||
} = body
|
||||
{
|
||||
let else_is_param = matches!(else_branch.as_ref(), Expr::Var(id) if id.name == param);
|
||||
if !else_is_param {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if then_branch is -param
|
||||
if let Expr::UnaryOp {
|
||||
op: ast::UnaryOp::Neg,
|
||||
operand,
|
||||
..
|
||||
} = then_branch.as_ref()
|
||||
{
|
||||
if matches!(operand.as_ref(), Expr::Var(id) if id.name == param) {
|
||||
// Check condition is param < 0
|
||||
if let Expr::BinaryOp {
|
||||
op: BinaryOp::Lt,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} = condition.as_ref()
|
||||
{
|
||||
let left_is_param = matches!(left.as_ref(), Expr::Var(id) if id.name == param);
|
||||
let right_is_zero =
|
||||
matches!(right.as_ref(), Expr::Literal(lit) if matches!(lit.kind, LiteralKind::Int(0)));
|
||||
if left_is_param && right_is_zero {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Check if a function body contains recursive calls to the function
|
||||
fn has_recursive_calls(func_name: &str, body: &Expr) -> bool {
|
||||
match body {
|
||||
Expr::Call { func, args, .. } => {
|
||||
// Check if this call is to the function itself
|
||||
if let Expr::Var(id) = func.as_ref() {
|
||||
if id.name == func_name {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Check in function expression and arguments
|
||||
has_recursive_calls(func_name, func)
|
||||
|| args.iter().any(|a| has_recursive_calls(func_name, a))
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
has_recursive_calls(func_name, left) || has_recursive_calls(func_name, right)
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => has_recursive_calls(func_name, operand),
|
||||
Expr::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
..
|
||||
} => {
|
||||
has_recursive_calls(func_name, condition)
|
||||
|| has_recursive_calls(func_name, then_branch)
|
||||
|| has_recursive_calls(func_name, else_branch)
|
||||
}
|
||||
Expr::Block { statements, result, .. } => {
|
||||
statements.iter().any(|s| match s {
|
||||
Statement::Let { value, .. } => has_recursive_calls(func_name, value),
|
||||
Statement::Expr(e) => has_recursive_calls(func_name, e),
|
||||
}) || has_recursive_calls(func_name, result)
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
has_recursive_calls(func_name, scrutinee)
|
||||
|| arms.iter().any(|a| has_recursive_calls(func_name, &a.body))
|
||||
}
|
||||
Expr::Lambda { body, .. } => has_recursive_calls(func_name, body),
|
||||
Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
|
||||
elements.iter().any(|e| has_recursive_calls(func_name, e))
|
||||
}
|
||||
Expr::Record { fields, .. } => {
|
||||
fields.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||
}
|
||||
Expr::Field { object, .. } => has_recursive_calls(func_name, object),
|
||||
Expr::Let { value, body, .. } => {
|
||||
has_recursive_calls(func_name, value) || has_recursive_calls(func_name, body)
|
||||
}
|
||||
Expr::Run { expr, handlers, .. } => {
|
||||
has_recursive_calls(func_name, expr)
|
||||
|| handlers.iter().any(|(_, e)| has_recursive_calls(func_name, e))
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find all recursive call arguments: returns Vec of argument lists
|
||||
fn find_recursive_call_args<'a>(func_name: &str, body: &'a Expr) -> Vec<&'a [Expr]> {
|
||||
let mut result = Vec::new();
|
||||
collect_recursive_call_args(func_name, body, &mut result);
|
||||
result
|
||||
}
|
||||
|
||||
fn collect_recursive_call_args<'a>(func_name: &str, body: &'a Expr, result: &mut Vec<&'a [Expr]>) {
|
||||
match body {
|
||||
Expr::Call { func, args, .. } => {
|
||||
if let Expr::Var(id) = func.as_ref() {
|
||||
if id.name == func_name {
|
||||
result.push(args.as_slice());
|
||||
}
|
||||
}
|
||||
collect_recursive_call_args(func_name, func, result);
|
||||
for arg in args {
|
||||
collect_recursive_call_args(func_name, arg, result);
|
||||
}
|
||||
}
|
||||
Expr::BinaryOp { left, right, .. } => {
|
||||
collect_recursive_call_args(func_name, left, result);
|
||||
collect_recursive_call_args(func_name, right, result);
|
||||
}
|
||||
Expr::UnaryOp { operand, .. } => {
|
||||
collect_recursive_call_args(func_name, operand, result);
|
||||
}
|
||||
Expr::If {
|
||||
condition,
|
||||
then_branch,
|
||||
else_branch,
|
||||
..
|
||||
} => {
|
||||
collect_recursive_call_args(func_name, condition, result);
|
||||
collect_recursive_call_args(func_name, then_branch, result);
|
||||
collect_recursive_call_args(func_name, else_branch, result);
|
||||
}
|
||||
Expr::Block { statements, result: res, .. } => {
|
||||
for s in statements {
|
||||
match s {
|
||||
Statement::Let { value, .. } => {
|
||||
collect_recursive_call_args(func_name, value, result)
|
||||
}
|
||||
Statement::Expr(e) => collect_recursive_call_args(func_name, e, result),
|
||||
}
|
||||
}
|
||||
collect_recursive_call_args(func_name, res, result);
|
||||
}
|
||||
Expr::Match { scrutinee, arms, .. } => {
|
||||
collect_recursive_call_args(func_name, scrutinee, result);
|
||||
for arm in arms {
|
||||
collect_recursive_call_args(func_name, &arm.body, result);
|
||||
}
|
||||
}
|
||||
Expr::Lambda { body, .. } => collect_recursive_call_args(func_name, body, result),
|
||||
Expr::Let { value, body, .. } => {
|
||||
collect_recursive_call_args(func_name, value, result);
|
||||
collect_recursive_call_args(func_name, body, result);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an argument is structurally decreasing from a parameter
|
||||
/// Recognizes patterns like: n - 1, n - k (for positive k)
|
||||
fn is_structurally_decreasing(arg: &Expr, param_name: &str) -> bool {
|
||||
match arg {
|
||||
// Pattern: n - 1, n - k for positive k
|
||||
Expr::BinaryOp {
|
||||
op: BinaryOp::Sub,
|
||||
left,
|
||||
right,
|
||||
..
|
||||
} => {
|
||||
if let Expr::Var(id) = left.as_ref() {
|
||||
if id.name == param_name {
|
||||
// Check if right side is a positive integer literal
|
||||
if let Expr::Literal(lit) = right.as_ref() {
|
||||
if let LiteralKind::Int(n) = lit.kind {
|
||||
return n > 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a function terminates (structural recursion check)
|
||||
fn check_termination(func: &FunctionDecl) -> Result<(), String> {
|
||||
// Non-recursive functions always terminate
|
||||
if !has_recursive_calls(&func.name.name, &func.body) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// For recursive functions, check structural recursion
|
||||
let param_names: Vec<&str> = func.params.iter().map(|p| p.name.name.as_str()).collect();
|
||||
let recursive_calls = find_recursive_call_args(&func.name.name, &func.body);
|
||||
|
||||
for call_args in recursive_calls {
|
||||
// At least one argument must be structurally decreasing
|
||||
let has_decreasing = call_args.iter().enumerate().any(|(i, arg)| {
|
||||
if i < param_names.len() {
|
||||
is_structurally_decreasing(arg, param_names[i])
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !has_decreasing {
|
||||
return Err("Recursive call has no provably decreasing argument".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Property constraint on a function parameter
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParamPropertyConstraint {
|
||||
pub param_name: String,
|
||||
pub required_properties: PropertySet,
|
||||
}
|
||||
|
||||
/// Type checker
|
||||
pub struct TypeChecker {
|
||||
env: TypeEnv,
|
||||
@@ -111,6 +494,14 @@ pub struct TypeChecker {
|
||||
errors: Vec<TypeError>,
|
||||
/// Type parameters in scope (maps "T" -> Type::Var(n) for generics)
|
||||
type_params: HashMap<String, Type>,
|
||||
/// Property constraints from where clauses: func_name -> Vec<(param_name, properties)>
|
||||
property_constraints: HashMap<String, Vec<ParamPropertyConstraint>>,
|
||||
/// Versioned type definitions: type_name -> version -> TypeDef
|
||||
versioned_types: HashMap<String, HashMap<u32, types::TypeDef>>,
|
||||
/// Latest version for each versioned type: type_name -> highest_version
|
||||
latest_versions: HashMap<String, u32>,
|
||||
/// Migrations: type_name -> source_version -> migration_body
|
||||
migrations: HashMap<String, HashMap<u32, Expr>>,
|
||||
}
|
||||
|
||||
impl TypeChecker {
|
||||
@@ -122,6 +513,10 @@ impl TypeChecker {
|
||||
inferring_effects: false,
|
||||
errors: Vec::new(),
|
||||
type_params: HashMap::new(),
|
||||
property_constraints: HashMap::new(),
|
||||
versioned_types: HashMap::new(),
|
||||
latest_versions: HashMap::new(),
|
||||
migrations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +675,32 @@ impl TypeChecker {
|
||||
let type_def = self.type_def(type_decl);
|
||||
self.env.types.insert(type_decl.name.name.clone(), type_def.clone());
|
||||
|
||||
// Track versioned types for schema evolution
|
||||
if let Some(version) = &type_decl.version {
|
||||
let type_name = type_decl.name.name.clone();
|
||||
let version_num = version.number;
|
||||
|
||||
// Store the type definition for this version
|
||||
self.versioned_types
|
||||
.entry(type_name.clone())
|
||||
.or_default()
|
||||
.insert(version_num, type_def.clone());
|
||||
|
||||
// Update latest version if this is higher
|
||||
let current_latest = self.latest_versions.get(&type_name).copied().unwrap_or(0);
|
||||
if version_num > current_latest {
|
||||
self.latest_versions.insert(type_name.clone(), version_num);
|
||||
}
|
||||
|
||||
// Register migrations (Phase 4)
|
||||
for migration in &type_decl.migrations {
|
||||
self.migrations
|
||||
.entry(type_name.clone())
|
||||
.or_default()
|
||||
.insert(migration.from_version.number, migration.body.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Register ADT constructors as values with polymorphic types
|
||||
if let ast::TypeDef::Enum(variants) = &type_decl.definition {
|
||||
for variant in variants {
|
||||
@@ -438,6 +859,89 @@ impl TypeChecker {
|
||||
});
|
||||
}
|
||||
|
||||
// Deterministic functions cannot use non-deterministic effects (Random, Time)
|
||||
if properties.contains(Property::Deterministic) {
|
||||
let non_det_effects: Vec<_> = effective_effects
|
||||
.effects
|
||||
.iter()
|
||||
.filter(|e| matches!(e.as_str(), "Random" | "Time"))
|
||||
.cloned()
|
||||
.collect();
|
||||
if !non_det_effects.is_empty() {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as deterministic but uses non-deterministic effects: {{{}}}",
|
||||
func.name.name,
|
||||
non_det_effects.join(", ")
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Commutative functions must have 2 params and use a commutative operation
|
||||
if properties.contains(Property::Commutative) {
|
||||
if func.params.len() != 2 {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as commutative but has {} parameters (expected 2)",
|
||||
func.name.name,
|
||||
func.params.len()
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
} else if !is_commutative_body(&func.body, &func.params[0].name.name, &func.params[1].name.name) {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as commutative but its body is not a commutative operation on its parameters",
|
||||
func.name.name
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Idempotent functions must satisfy f(f(x)) == f(x)
|
||||
// We verify this through pattern recognition
|
||||
if properties.contains(Property::Idempotent) {
|
||||
if !is_idempotent_body(&func.body, &func.params) {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as idempotent but could not be verified. \
|
||||
Recognized patterns: identity, constants, clamping, projections, abs. \
|
||||
Use 'assume is idempotent' if you're certain it is idempotent.",
|
||||
func.name.name
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Total functions must terminate and cannot fail
|
||||
if properties.is_total() {
|
||||
// Check 1: Cannot use Fail effect
|
||||
if effective_effects.contains("Fail") {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as total but uses the Fail effect",
|
||||
func.name.name
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
}
|
||||
|
||||
// Check 2: Must terminate (structural recursion)
|
||||
if let Err(reason) = check_termination(func) {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Function '{}' is declared as total but may not terminate: {}",
|
||||
func.name.name, reason
|
||||
),
|
||||
span: func.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If effects were declared, verify that inferred effects are a subset
|
||||
if explicit_effects && !inferred.is_subset(&declared_effects) {
|
||||
let missing: Vec<_> = inferred
|
||||
@@ -465,9 +969,35 @@ impl TypeChecker {
|
||||
property,
|
||||
span,
|
||||
} => {
|
||||
// Record the constraint for later checking when the function is called
|
||||
// For now, we just validate that the type parameter exists
|
||||
if !func.type_params.iter().any(|p| p.name == type_param.name)
|
||||
// Find which parameter has this type and record the constraint
|
||||
let param_with_type = func.params.iter().find(|p| {
|
||||
// Check if param's type is the type parameter
|
||||
if let TypeExpr::Named(name) = &p.typ {
|
||||
name.name == type_param.name
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(param) = param_with_type {
|
||||
// Record the constraint for checking at call sites
|
||||
let constraints = self
|
||||
.property_constraints
|
||||
.entry(func.name.name.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
// Check if we already have a constraint for this param
|
||||
if let Some(existing) = constraints.iter_mut().find(|c| c.param_name == param.name.name) {
|
||||
existing.required_properties.insert(Property::from(*property));
|
||||
} else {
|
||||
let mut props = PropertySet::empty();
|
||||
props.insert(Property::from(*property));
|
||||
constraints.push(ParamPropertyConstraint {
|
||||
param_name: param.name.name.clone(),
|
||||
required_properties: props,
|
||||
});
|
||||
}
|
||||
} else if !func.type_params.iter().any(|p| p.name == type_param.name)
|
||||
&& !func.params.iter().any(|p| p.name.name == type_param.name)
|
||||
{
|
||||
self.errors.push(TypeError {
|
||||
@@ -525,7 +1055,8 @@ impl TypeChecker {
|
||||
fn check_let_decl(&mut self, let_decl: &LetDecl) {
|
||||
let inferred = self.infer_expr(&let_decl.value);
|
||||
|
||||
if let Some(ref type_expr) = let_decl.typ {
|
||||
// Use the declared type if present, otherwise use inferred
|
||||
let final_type = if let Some(ref type_expr) = let_decl.typ {
|
||||
let declared = self.resolve_type(type_expr);
|
||||
if let Err(e) = unify(&inferred, &declared) {
|
||||
self.errors.push(TypeError {
|
||||
@@ -536,10 +1067,14 @@ impl TypeChecker {
|
||||
span: let_decl.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
// Use declared type (preserves version annotations)
|
||||
declared
|
||||
} else {
|
||||
inferred
|
||||
};
|
||||
|
||||
// Update the binding with the inferred type
|
||||
let scheme = self.env.generalize(&inferred);
|
||||
// Update the binding with the final type
|
||||
let scheme = self.env.generalize(&final_type);
|
||||
self.env.bind(&let_decl.name.name, scheme);
|
||||
}
|
||||
|
||||
@@ -870,6 +1405,48 @@ impl TypeChecker {
|
||||
let func_type = self.infer_expr(func);
|
||||
let arg_types: Vec<Type> = args.iter().map(|a| self.infer_expr(a)).collect();
|
||||
|
||||
// Check property constraints from where clauses
|
||||
if let Expr::Var(func_id) = func {
|
||||
if let Some(constraints) = self.property_constraints.get(&func_id.name).cloned() {
|
||||
// Get parameter names from the function declaration (if available in env)
|
||||
// We'll match by position since we have the constraints by param name
|
||||
if let Some(scheme) = self.env.lookup(&func_id.name) {
|
||||
let func_typ = scheme.instantiate();
|
||||
if let Type::Function { params: param_types, .. } = &func_typ {
|
||||
for constraint in &constraints {
|
||||
// Find which argument position corresponds to this param
|
||||
// For now, match by position based on stored param names
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
// Get the properties of the argument
|
||||
let arg_props = self.get_expr_properties(arg);
|
||||
|
||||
// Check if this argument corresponds to a constrained param
|
||||
// We check all constraints and verify the arg satisfies them
|
||||
if !arg_props.satisfies(&constraint.required_properties) {
|
||||
// Only report if this argument could be the constrained one
|
||||
// (simple heuristic: function type argument)
|
||||
if i < param_types.len() {
|
||||
if let Type::Function { .. } = ¶m_types[i] {
|
||||
self.errors.push(TypeError {
|
||||
message: format!(
|
||||
"Argument to '{}' does not satisfy property constraint: \
|
||||
expected {:?}, but argument has {:?}",
|
||||
func_id.name,
|
||||
constraint.required_properties,
|
||||
arg_props
|
||||
),
|
||||
span: arg.span(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let result_type = Type::var();
|
||||
// Include current effects in the expected function type
|
||||
// This allows calling functions that require effects when those effects are available
|
||||
@@ -891,6 +1468,26 @@ impl TypeChecker {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the behavioral properties of an expression (conservative)
|
||||
fn get_expr_properties(&self, expr: &Expr) -> PropertySet {
|
||||
match expr {
|
||||
Expr::Var(id) => {
|
||||
// Look up the function and get its properties
|
||||
if let Some(scheme) = self.env.lookup(&id.name) {
|
||||
let typ = scheme.instantiate();
|
||||
if let Type::Function { properties, .. } = typ {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
PropertySet::empty()
|
||||
}
|
||||
// Lambdas: could analyze but for now be conservative
|
||||
Expr::Lambda { .. } => PropertySet::empty(),
|
||||
// Other expressions: no properties
|
||||
_ => PropertySet::empty(),
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_effect_op(
|
||||
&mut self,
|
||||
effect: &Ident,
|
||||
@@ -935,7 +1532,7 @@ impl TypeChecker {
|
||||
}
|
||||
|
||||
// Built-in effects are always available
|
||||
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http"];
|
||||
let builtin_effects = ["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"];
|
||||
let is_builtin = builtin_effects.contains(&effect.name.as_str());
|
||||
|
||||
// Track this effect for inference
|
||||
@@ -1440,7 +2037,7 @@ impl TypeChecker {
|
||||
|
||||
// Built-in effects are always available in run blocks (they have runtime implementations)
|
||||
let builtin_effects: EffectSet =
|
||||
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http"].iter().map(|s| s.to_string()));
|
||||
EffectSet::from_iter(["Console", "Fail", "State", "Reader", "Random", "Time", "File", "Process", "Http", "HttpServer"].iter().map(|s| s.to_string()));
|
||||
|
||||
// Extend current effects with handled ones and built-in effects
|
||||
let combined = self.current_effects.union(&handled_effects).union(&builtin_effects);
|
||||
@@ -1796,13 +2393,18 @@ impl TypeChecker {
|
||||
.collect(),
|
||||
),
|
||||
TypeExpr::Unit => Type::Unit,
|
||||
TypeExpr::Versioned {
|
||||
base,
|
||||
constraint: _,
|
||||
} => {
|
||||
// For now, resolve the base type and ignore versioning
|
||||
// Full version tracking will be added in the type system
|
||||
self.resolve_type(base)
|
||||
TypeExpr::Versioned { base, constraint } => {
|
||||
// Resolve the base type and preserve version information
|
||||
let base_type = self.resolve_type(base);
|
||||
let version_info = match constraint {
|
||||
ast::VersionConstraint::Exact(v) => VersionInfo::Exact(v.number),
|
||||
ast::VersionConstraint::AtLeast(v) => VersionInfo::AtLeast(v.number),
|
||||
ast::VersionConstraint::Latest(_) => VersionInfo::Latest,
|
||||
};
|
||||
Type::Versioned {
|
||||
base: Box::new(base_type),
|
||||
version: version_info,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user