Skip to main content

tl_types/
infer.rs

1// ThinkingLanguage — Expression Type Inference
2// Licensed under MIT OR Apache-2.0
3//
4// Infers types from expressions using local, forward-only rules.
5
6use tl_ast::{BinOp, Expr, UnaryOp};
7
8use crate::convert::convert_type_expr;
9use crate::{Type, TypeEnv};
10
11/// Infer the type of an expression given the current type environment.
12pub fn infer_expr(expr: &Expr, env: &TypeEnv) -> Type {
13    match expr {
14        // Literals
15        Expr::Int(_) => Type::Int,
16        Expr::Float(_) => Type::Float,
17        Expr::String(_) => Type::String,
18        Expr::Bool(_) => Type::Bool,
19        Expr::None => Type::None,
20
21        // Variable reference
22        Expr::Ident(name) => env.lookup(name).cloned().unwrap_or(Type::Any),
23
24        // Binary operations
25        Expr::BinOp { left, op, right } => infer_binop(left, op, right, env),
26
27        // Unary operations
28        Expr::UnaryOp { op, expr } => infer_unaryop(op, expr, env),
29
30        // Function call — including method calls (Call { function: Member { .. }, .. })
31        Expr::Call { function, args } => {
32            // Method call: obj.method(args)
33            if let Expr::Member { object, field } = function.as_ref() {
34                let obj_ty = infer_expr(object, env);
35                return infer_method_call(&obj_ty, field, args, env);
36            }
37
38            if let Expr::Ident(name) = function.as_ref() {
39                // Check known builtins
40                match name.as_str() {
41                    "Ok" => Type::Result(Box::new(Type::Any), Box::new(Type::Any)),
42                    "Err" => Type::Result(Box::new(Type::Any), Box::new(Type::Any)),
43                    "is_ok" | "is_err" => Type::Bool,
44                    "unwrap" => Type::Any,
45                    "set_from" => Type::Set(Box::new(Type::Any)),
46                    "len" | "int" => Type::Int,
47                    "float" => Type::Float,
48                    "str" | "type_of" => Type::String,
49                    "bool" => Type::Bool,
50                    // Phase 13: more builtin return types
51                    "range" => Type::List(Box::new(Type::Int)),
52                    "print" | "println" | "push" | "append" | "write_file" | "append_file" => {
53                        Type::Unit
54                    }
55                    "split" => Type::List(Box::new(Type::String)),
56                    "json_parse" => Type::Any,
57                    "json_stringify" | "read_file" => Type::String,
58                    "channel" => Type::Channel(Box::new(Type::Any)),
59                    "spawn" => Type::Task(Box::new(Type::Any)),
60                    "map" => {
61                        // map(list, fn) -> list
62                        if !args.is_empty() {
63                            let arg_ty = infer_expr(&args[0], env);
64                            match arg_ty {
65                                Type::List(_) => arg_ty,
66                                _ => Type::List(Box::new(Type::Any)),
67                            }
68                        } else {
69                            Type::List(Box::new(Type::Any))
70                        }
71                    }
72                    "filter" => {
73                        if !args.is_empty() {
74                            let arg_ty = infer_expr(&args[0], env);
75                            match arg_ty {
76                                Type::List(_) => arg_ty,
77                                _ => Type::List(Box::new(Type::Any)),
78                            }
79                        } else {
80                            Type::List(Box::new(Type::Any))
81                        }
82                    }
83                    "now" => Type::String,
84                    "date_format" | "date_parse" => Type::String,
85                    "regex_match" => Type::Bool,
86                    "regex_find" => Type::List(Box::new(Type::String)),
87                    "sleep" => Type::Unit,
88                    "env_get" => Type::String,
89                    "env_set" => Type::Unit,
90                    "send" | "recv" | "try_recv" => Type::Any,
91                    "await_all" => Type::List(Box::new(Type::Any)),
92                    "collect" | "gen_collect" => Type::List(Box::new(Type::Any)),
93                    "iter" => Type::Generator(Box::new(Type::Any)),
94                    "next" => Type::Any,
95                    "is_generator" | "file_exists" => Type::Bool,
96                    "assert" | "assert_eq" => Type::Unit,
97                    // Phase 15: Data Quality & Connectors
98                    "fill_null" | "drop_null" | "dedup" | "clamp" | "data_profile"
99                    | "read_mysql" => Type::Table {
100                        name: None,
101                        columns: None,
102                    },
103                    // Phase 22: Advanced Types
104                    "decimal" => Type::Decimal,
105                    "tensor" | "tensor_zeros" | "tensor_ones" | "tensor_reshape"
106                    | "tensor_transpose" | "tensor_dot" => Type::Tensor,
107                    "tensor_shape" => Type::List(Box::new(Type::Int)),
108                    "tensor_sum" | "tensor_mean" => Type::Float,
109                    "row_count" | "levenshtein" => Type::Int,
110                    "null_rate" => Type::Float,
111                    "is_unique" | "is_email" | "is_url" | "is_phone" | "is_between" => Type::Bool,
112                    "soundex" | "redis_connect" => Type::String,
113                    "graphql_query" => Type::Any,
114                    "redis_get" => Type::Any,
115                    "redis_set" | "redis_del" | "register_s3" => Type::Unit,
116                    // Phase 21: Schema Evolution
117                    "schema_register" => Type::Unit,
118                    "schema_get" | "schema_latest" => Type::Any,
119                    "schema_history" | "schema_versions" => Type::List(Box::new(Type::Int)),
120                    "schema_check" | "schema_diff" | "schema_fields" => {
121                        Type::List(Box::new(Type::String))
122                    }
123                    "schema_apply_migration" => Type::Unit,
124                    // Phase 20: Python FFI
125                    "py_import" => Type::PyObject,
126                    "py_eval" | "py_call" | "py_getattr" | "py_to_tl" => Type::Any,
127                    "py_setattr" => Type::Unit,
128                    // Phase 23: Security & Access Control
129                    "secret_get" => Type::String,
130                    "secret_set" | "secret_delete" => Type::Unit,
131                    "secret_list" => Type::List(Box::new(Type::String)),
132                    "check_permission" => Type::Bool,
133                    "mask_email" | "mask_phone" | "mask_cc" | "redact" | "hash" => Type::String,
134                    // Phase 24: Async/Await
135                    "async_read_file" | "async_http_get" | "async_http_post" => {
136                        Type::Task(Box::new(Type::String))
137                    }
138                    "async_write_file" | "async_sleep" => Type::Task(Box::new(Type::Unit)),
139                    "select" | "race_all" => Type::Any,
140                    "async_map" => Type::List(Box::new(Type::Any)),
141                    "async_filter" => Type::List(Box::new(Type::Any)),
142                    _ => {
143                        if let Some(sig) = env.lookup_fn(name) {
144                            sig.ret.clone()
145                        } else {
146                            Type::Any
147                        }
148                    }
149                }
150            } else {
151                // Calling a closure or other expression
152                let fn_ty = infer_expr(function, env);
153                match fn_ty {
154                    Type::Function { ret, .. } => *ret,
155                    _ => Type::Any,
156                }
157            }
158        }
159
160        // List literal
161        Expr::List(elements) => {
162            if elements.is_empty() {
163                Type::List(Box::new(Type::Any))
164            } else {
165                let elem_ty = infer_expr(&elements[0], env);
166                Type::List(Box::new(elem_ty))
167            }
168        }
169
170        // Map literal — infer value type from first entry
171        Expr::Map(entries) => {
172            if entries.is_empty() {
173                Type::Map(Box::new(Type::Any))
174            } else {
175                let val_ty = infer_expr(&entries[0].1, env);
176                Type::Map(Box::new(val_ty))
177            }
178        }
179
180        // Member access — look up struct field types and known type methods
181        Expr::Member { object, field } => {
182            let obj_ty = infer_expr(object, env);
183            infer_member_access(&obj_ty, field, env)
184        }
185
186        // Index access
187        Expr::Index { object, .. } => {
188            let obj_ty = infer_expr(object, env);
189            match obj_ty {
190                Type::List(inner) => *inner,
191                Type::Map(inner) => *inner,
192                _ => Type::Any,
193            }
194        }
195
196        // Closure — infer param types from annotations and return type from body
197        Expr::Closure {
198            params,
199            body,
200            return_type,
201            ..
202        } => {
203            let param_types: Vec<Type> = params
204                .iter()
205                .map(|p| {
206                    p.type_ann
207                        .as_ref()
208                        .map(convert_type_expr)
209                        .unwrap_or(Type::Any)
210                })
211                .collect();
212            let ret = match body {
213                tl_ast::ClosureBody::Expr(e) => infer_expr(e, env),
214                tl_ast::ClosureBody::Block { expr: Some(e), .. } => infer_expr(e, env),
215                tl_ast::ClosureBody::Block { expr: None, .. } => {
216                    // If no tail expr, use return_type annotation or None
217                    return_type
218                        .as_ref()
219                        .map(convert_type_expr)
220                        .unwrap_or(Type::None)
221                }
222            };
223            Type::Function {
224                params: param_types,
225                ret: Box::new(ret),
226            }
227        }
228
229        // Null coalesce: option<T> ?? T -> T
230        Expr::NullCoalesce { expr, default } => {
231            let expr_ty = infer_expr(expr, env);
232            match expr_ty {
233                Type::Option(inner) => *inner,
234                _ => infer_expr(default, env),
235            }
236        }
237
238        // Try operator: result<T,E>? -> T
239        Expr::Try(inner) => {
240            let inner_ty = infer_expr(inner, env);
241            match inner_ty {
242                Type::Result(ok, _) => *ok,
243                Type::Option(inner_t) => *inner_t,
244                _ => Type::Any,
245            }
246        }
247
248        // Await
249        Expr::Await(inner) => {
250            let inner_ty = infer_expr(inner, env);
251            match inner_ty {
252                Type::Task(inner) => *inner,
253                _ => Type::Any,
254            }
255        }
256
257        // Yield
258        Expr::Yield(_) => Type::Any,
259
260        // Range
261        Expr::Range { .. } => Type::List(Box::new(Type::Int)),
262
263        // Pipe
264        Expr::Pipe { right, .. } => infer_expr(right, env),
265
266        // Block
267        Expr::Block { expr, .. } => {
268            if let Some(e) = expr {
269                infer_expr(e, env)
270            } else {
271                Type::Unit
272            }
273        }
274
275        // Assignment
276        Expr::Assign { value, .. } => infer_expr(value, env),
277
278        // Struct init
279        Expr::StructInit { name, .. } => Type::Struct(name.clone()),
280
281        // Enum variant
282        Expr::EnumVariant { enum_name, .. } => Type::Enum(enum_name.clone()),
283
284        // Match/Case
285        Expr::Match { arms, .. } | Expr::Case { arms } => {
286            if let Some(arm) = arms.first() {
287                infer_expr(&arm.body, env)
288            } else {
289                Type::Any
290            }
291        }
292
293        _ => Type::Any,
294    }
295}
296
297/// Infer the type of a member access (obj.field).
298fn infer_member_access(obj_ty: &Type, field: &str, env: &TypeEnv) -> Type {
299    match obj_ty {
300        Type::Struct(name) => {
301            if let Some(fields) = env.lookup_struct(name) {
302                fields
303                    .iter()
304                    .find(|(f, _)| f == field)
305                    .map(|(_, ty)| ty.clone())
306                    .unwrap_or(Type::Any)
307            } else {
308                Type::Any
309            }
310        }
311        Type::String => match field {
312            "len" | "length" => Type::Int,
313            "chars" => Type::List(Box::new(Type::String)),
314            "split" => Type::Function {
315                params: vec![Type::String],
316                ret: Box::new(Type::List(Box::new(Type::String))),
317            },
318            "trim" | "upper" | "lower" | "reverse" | "repeat" | "substring" | "pad_left"
319            | "pad_right" => Type::String,
320            "contains" | "starts_with" | "ends_with" => Type::Bool,
321            "index_of" => Type::Int,
322            _ => Type::Any,
323        },
324        Type::List(inner) => match field {
325            "len" | "length" => Type::Int,
326            "contains" => Type::Bool,
327            "index_of" => Type::Int,
328            "sort" | "reverse" | "slice" | "flat_map" => Type::List(inner.clone()),
329            "first" | "last" => *inner.clone(),
330            _ => Type::Any,
331        },
332        Type::Map(_) => match field {
333            "len" => Type::Int,
334            "keys" => Type::List(Box::new(Type::String)),
335            "values" => Type::List(Box::new(Type::Any)),
336            "contains_key" => Type::Bool,
337            _ => Type::Any,
338        },
339        Type::Set(inner) => match field {
340            "len" => Type::Int,
341            "contains" => Type::Bool,
342            "union" | "intersection" | "difference" => Type::Set(inner.clone()),
343            _ => Type::Any,
344        },
345        _ => Type::Any,
346    }
347}
348
349/// Infer the return type of a method call (obj.method(args)).
350fn infer_method_call(obj_ty: &Type, method: &str, _args: &[Expr], env: &TypeEnv) -> Type {
351    match obj_ty {
352        Type::String => match method {
353            "len" | "length" | "index_of" => Type::Int,
354            "split" => Type::List(Box::new(Type::String)),
355            "chars" => Type::List(Box::new(Type::String)),
356            "trim" | "upper" | "lower" | "reverse" | "repeat" | "replace" | "substring"
357            | "pad_left" | "pad_right" => Type::String,
358            "contains" | "starts_with" | "ends_with" => Type::Bool,
359            _ => Type::Any,
360        },
361        Type::List(inner) => match method {
362            "len" | "length" | "index_of" => Type::Int,
363            "contains" => Type::Bool,
364            "push" | "append" => Type::Unit,
365            "map" | "filter" | "sort" | "reverse" | "slice" | "flat_map" => {
366                Type::List(inner.clone())
367            }
368            "sum" => *inner.clone(),
369            "collect" => Type::List(inner.clone()),
370            "join" => Type::String,
371            "first" | "last" => *inner.clone(),
372            _ => Type::Any,
373        },
374        Type::Map(_val_ty) => match method {
375            "len" => Type::Int,
376            "keys" => Type::List(Box::new(Type::String)),
377            "values" => Type::List(Box::new(Type::Any)),
378            "contains_key" => Type::Bool,
379            "remove" => Type::Unit,
380            _ => Type::Any,
381        },
382        Type::Set(inner) => match method {
383            "len" => Type::Int,
384            "contains" => Type::Bool,
385            "add" | "remove" => Type::Unit,
386            "union" | "intersection" | "difference" => Type::Set(inner.clone()),
387            _ => Type::Any,
388        },
389        Type::Generator(inner) => match method {
390            "next" => *inner.clone(),
391            "collect" => Type::List(inner.clone()),
392            _ => Type::Any,
393        },
394        Type::Struct(name) => {
395            // Look up method in impl blocks via the function registry
396            let mangled = format!("{name}::{method}");
397            if let Some(sig) = env.lookup_fn(&mangled) {
398                sig.ret.clone()
399            } else {
400                Type::Any
401            }
402        }
403        _ => Type::Any,
404    }
405}
406
407fn infer_binop(left: &Expr, op: &BinOp, right: &Expr, env: &TypeEnv) -> Type {
408    let left_ty = infer_expr(left, env);
409    let right_ty = infer_expr(right, env);
410
411    match op {
412        // Arithmetic
413        BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
414            match (&left_ty, &right_ty) {
415                (Type::Int, Type::Int) => Type::Int,
416                (Type::Float, Type::Float)
417                | (Type::Int, Type::Float)
418                | (Type::Float, Type::Int) => Type::Float,
419                // Decimal arithmetic
420                (Type::Decimal, Type::Decimal)
421                | (Type::Decimal, Type::Int)
422                | (Type::Int, Type::Decimal) => Type::Decimal,
423                // Decimal + Float => Float
424                (Type::Decimal, Type::Float) | (Type::Float, Type::Decimal) => Type::Float,
425                (Type::String, Type::String) if matches!(op, BinOp::Add) => Type::String,
426                _ => {
427                    if matches!(left_ty, Type::Any) || matches!(right_ty, Type::Any) {
428                        Type::Any
429                    } else {
430                        Type::Error
431                    }
432                }
433            }
434        }
435        // Comparison
436        BinOp::Eq | BinOp::Neq | BinOp::Lt | BinOp::Gt | BinOp::Lte | BinOp::Gte => Type::Bool,
437        // Logical
438        BinOp::And | BinOp::Or => Type::Bool,
439    }
440}
441
442fn infer_unaryop(op: &UnaryOp, expr: &Expr, env: &TypeEnv) -> Type {
443    let inner_ty = infer_expr(expr, env);
444    match op {
445        UnaryOp::Neg => match inner_ty {
446            Type::Int => Type::Int,
447            Type::Float => Type::Float,
448            _ => Type::Any,
449        },
450        UnaryOp::Not => Type::Bool,
451        UnaryOp::Ref => Type::Ref(Box::new(inner_ty)),
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458    use crate::FnSig;
459
460    #[test]
461    fn test_infer_literals() {
462        let env = TypeEnv::new();
463        assert_eq!(infer_expr(&Expr::Int(42), &env), Type::Int);
464        assert_eq!(infer_expr(&Expr::Float(3.14), &env), Type::Float);
465        assert_eq!(
466            infer_expr(&Expr::String("hello".into()), &env),
467            Type::String
468        );
469        assert_eq!(infer_expr(&Expr::Bool(true), &env), Type::Bool);
470        assert_eq!(infer_expr(&Expr::None, &env), Type::None);
471    }
472
473    #[test]
474    fn test_infer_binop_arithmetic() {
475        let env = TypeEnv::new();
476        let expr = Expr::BinOp {
477            left: Box::new(Expr::Int(1)),
478            op: BinOp::Add,
479            right: Box::new(Expr::Int(2)),
480        };
481        assert_eq!(infer_expr(&expr, &env), Type::Int);
482
483        let expr = Expr::BinOp {
484            left: Box::new(Expr::Int(1)),
485            op: BinOp::Add,
486            right: Box::new(Expr::Float(2.0)),
487        };
488        assert_eq!(infer_expr(&expr, &env), Type::Float);
489    }
490
491    #[test]
492    fn test_infer_binop_comparison() {
493        let env = TypeEnv::new();
494        let expr = Expr::BinOp {
495            left: Box::new(Expr::Int(1)),
496            op: BinOp::Lt,
497            right: Box::new(Expr::Int(2)),
498        };
499        assert_eq!(infer_expr(&expr, &env), Type::Bool);
500    }
501
502    #[test]
503    fn test_infer_variable() {
504        let mut env = TypeEnv::new();
505        env.define("x".into(), Type::Int);
506        assert_eq!(infer_expr(&Expr::Ident("x".into()), &env), Type::Int);
507        // Unknown variable -> Any
508        assert_eq!(infer_expr(&Expr::Ident("y".into()), &env), Type::Any);
509    }
510
511    #[test]
512    fn test_infer_list() {
513        let env = TypeEnv::new();
514        let expr = Expr::List(vec![Expr::Int(1), Expr::Int(2)]);
515        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::Int)));
516
517        let empty = Expr::List(vec![]);
518        assert_eq!(infer_expr(&empty, &env), Type::List(Box::new(Type::Any)));
519    }
520
521    #[test]
522    fn test_infer_null_coalesce() {
523        let mut env = TypeEnv::new();
524        env.define("x".into(), Type::Option(Box::new(Type::Int)));
525        let expr = Expr::NullCoalesce {
526            expr: Box::new(Expr::Ident("x".into())),
527            default: Box::new(Expr::Int(0)),
528        };
529        assert_eq!(infer_expr(&expr, &env), Type::Int);
530    }
531
532    // ── Phase 13: Enhanced Inference Tests ──────────────────────
533
534    #[test]
535    fn test_infer_struct_field_access() {
536        let mut env = TypeEnv::new();
537        env.define_struct(
538            "Point".into(),
539            vec![("x".into(), Type::Int), ("y".into(), Type::Float)],
540        );
541        env.define("p".into(), Type::Struct("Point".into()));
542
543        let expr = Expr::Member {
544            object: Box::new(Expr::Ident("p".into())),
545            field: "x".into(),
546        };
547        assert_eq!(infer_expr(&expr, &env), Type::Int);
548
549        let expr = Expr::Member {
550            object: Box::new(Expr::Ident("p".into())),
551            field: "y".into(),
552        };
553        assert_eq!(infer_expr(&expr, &env), Type::Float);
554    }
555
556    #[test]
557    fn test_infer_nested_member_access() {
558        let mut env = TypeEnv::new();
559        env.define_struct("Inner".into(), vec![("val".into(), Type::Int)]);
560        env.define_struct(
561            "Outer".into(),
562            vec![("inner".into(), Type::Struct("Inner".into()))],
563        );
564        env.define("o".into(), Type::Struct("Outer".into()));
565
566        // o.inner should be Struct("Inner")
567        let inner_access = Expr::Member {
568            object: Box::new(Expr::Ident("o".into())),
569            field: "inner".into(),
570        };
571        assert_eq!(
572            infer_expr(&inner_access, &env),
573            Type::Struct("Inner".into())
574        );
575
576        // o.inner.val should be Int
577        let nested = Expr::Member {
578            object: Box::new(inner_access),
579            field: "val".into(),
580        };
581        assert_eq!(infer_expr(&nested, &env), Type::Int);
582    }
583
584    #[test]
585    fn test_infer_list_method_call() {
586        let mut env = TypeEnv::new();
587        env.define("xs".into(), Type::List(Box::new(Type::Int)));
588
589        // xs.len() -> int
590        let expr = Expr::Call {
591            function: Box::new(Expr::Member {
592                object: Box::new(Expr::Ident("xs".into())),
593                field: "len".into(),
594            }),
595            args: vec![],
596        };
597        assert_eq!(infer_expr(&expr, &env), Type::Int);
598
599        // xs.contains(1) -> bool
600        let expr = Expr::Call {
601            function: Box::new(Expr::Member {
602                object: Box::new(Expr::Ident("xs".into())),
603                field: "contains".into(),
604            }),
605            args: vec![Expr::Int(1)],
606        };
607        assert_eq!(infer_expr(&expr, &env), Type::Bool);
608    }
609
610    #[test]
611    fn test_infer_string_method_call() {
612        let mut env = TypeEnv::new();
613        env.define("s".into(), Type::String);
614
615        // s.split(",") -> list<string>
616        let expr = Expr::Call {
617            function: Box::new(Expr::Member {
618                object: Box::new(Expr::Ident("s".into())),
619                field: "split".into(),
620            }),
621            args: vec![Expr::String(",".into())],
622        };
623        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::String)));
624
625        // s.len() -> int
626        let expr = Expr::Call {
627            function: Box::new(Expr::Member {
628                object: Box::new(Expr::Ident("s".into())),
629                field: "len".into(),
630            }),
631            args: vec![],
632        };
633        assert_eq!(infer_expr(&expr, &env), Type::Int);
634    }
635
636    #[test]
637    fn test_infer_closure_with_annotations() {
638        let env = TypeEnv::new();
639        let expr = Expr::Closure {
640            params: vec![tl_ast::Param {
641                name: "x".into(),
642                type_ann: Some(tl_ast::TypeExpr::Named("int".into())),
643            }],
644            return_type: None,
645            body: tl_ast::ClosureBody::Expr(Box::new(Expr::BinOp {
646                left: Box::new(Expr::Ident("x".into())),
647                op: BinOp::Mul,
648                right: Box::new(Expr::Int(2)),
649            })),
650        };
651        let ty = infer_expr(&expr, &env);
652        match ty {
653            Type::Function { params, .. } => {
654                assert_eq!(params, vec![Type::Int]);
655            }
656            other => panic!("Expected function type, got {other}"),
657        }
658    }
659
660    #[test]
661    fn test_infer_closure_without_annotations() {
662        let env = TypeEnv::new();
663        let expr = Expr::Closure {
664            params: vec![tl_ast::Param {
665                name: "x".into(),
666                type_ann: None,
667            }],
668            return_type: None,
669            body: tl_ast::ClosureBody::Expr(Box::new(Expr::Int(42))),
670        };
671        let ty = infer_expr(&expr, &env);
672        match ty {
673            Type::Function { params, ret } => {
674                assert_eq!(params, vec![Type::Any]);
675                assert_eq!(*ret, Type::Int);
676            }
677            other => panic!("Expected function type, got {other}"),
678        }
679    }
680
681    #[test]
682    fn test_infer_map_literal() {
683        let env = TypeEnv::new();
684
685        // Non-empty map: infer value type from first entry
686        let expr = Expr::Map(vec![
687            (Expr::String("a".into()), Expr::Int(1)),
688            (Expr::String("b".into()), Expr::Int(2)),
689        ]);
690        assert_eq!(infer_expr(&expr, &env), Type::Map(Box::new(Type::Int)));
691
692        // Empty map
693        let empty = Expr::Map(vec![]);
694        assert_eq!(infer_expr(&empty, &env), Type::Map(Box::new(Type::Any)));
695    }
696
697    #[test]
698    fn test_infer_builtin_return_types() {
699        let env = TypeEnv::new();
700
701        // range -> list<int>
702        let expr = Expr::Call {
703            function: Box::new(Expr::Ident("range".into())),
704            args: vec![Expr::Int(0), Expr::Int(10)],
705        };
706        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::Int)));
707
708        // split -> list<string>
709        let expr = Expr::Call {
710            function: Box::new(Expr::Ident("split".into())),
711            args: vec![Expr::String("a,b".into()), Expr::String(",".into())],
712        };
713        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::String)));
714
715        // channel -> channel<any>
716        let expr = Expr::Call {
717            function: Box::new(Expr::Ident("channel".into())),
718            args: vec![],
719        };
720        assert_eq!(infer_expr(&expr, &env), Type::Channel(Box::new(Type::Any)));
721
722        // spawn -> task<any>
723        let expr = Expr::Call {
724            function: Box::new(Expr::Ident("spawn".into())),
725            args: vec![],
726        };
727        assert_eq!(infer_expr(&expr, &env), Type::Task(Box::new(Type::Any)));
728    }
729
730    #[test]
731    fn test_infer_unknown_member_returns_any() {
732        let mut env = TypeEnv::new();
733        env.define("p".into(), Type::Struct("Point".into()));
734
735        // Unknown struct (not registered) — returns Any
736        let expr = Expr::Member {
737            object: Box::new(Expr::Ident("p".into())),
738            field: "z".into(),
739        };
740        assert_eq!(infer_expr(&expr, &env), Type::Any);
741    }
742
743    #[test]
744    fn test_infer_user_defined_fn_return_type() {
745        let mut env = TypeEnv::new();
746        env.define_fn(
747            "my_fn".into(),
748            FnSig {
749                params: vec![("x".into(), Type::Int)],
750                ret: Type::String,
751            },
752        );
753
754        let expr = Expr::Call {
755            function: Box::new(Expr::Ident("my_fn".into())),
756            args: vec![Expr::Int(42)],
757        };
758        assert_eq!(infer_expr(&expr, &env), Type::String);
759    }
760
761    // Phase 21: Schema Evolution type inference
762
763    #[test]
764    fn test_infer_schema_register_returns_unit() {
765        let env = TypeEnv::new();
766        let expr = Expr::Call {
767            function: Box::new(Expr::Ident("schema_register".into())),
768            args: vec![],
769        };
770        assert_eq!(infer_expr(&expr, &env), Type::Unit);
771    }
772
773    #[test]
774    fn test_infer_schema_history_returns_list_int() {
775        let env = TypeEnv::new();
776        let expr = Expr::Call {
777            function: Box::new(Expr::Ident("schema_history".into())),
778            args: vec![],
779        };
780        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::Int)));
781    }
782
783    #[test]
784    fn test_infer_schema_check_returns_list_string() {
785        let env = TypeEnv::new();
786        let expr = Expr::Call {
787            function: Box::new(Expr::Ident("schema_check".into())),
788            args: vec![],
789        };
790        assert_eq!(infer_expr(&expr, &env), Type::List(Box::new(Type::String)));
791    }
792}