1use tl_ast::{BinOp, Expr, UnaryOp};
7
8use crate::convert::convert_type_expr;
9use crate::{Type, TypeEnv};
10
11pub fn infer_expr(expr: &Expr, env: &TypeEnv) -> Type {
13 match expr {
14 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 Expr::Ident(name) => env.lookup(name).cloned().unwrap_or(Type::Any),
23
24 Expr::BinOp { left, op, right } => infer_binop(left, op, right, env),
26
27 Expr::UnaryOp { op, expr } => infer_unaryop(op, expr, env),
29
30 Expr::Call { function, args } => {
32 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 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 "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 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 "fill_null" | "drop_null" | "dedup" | "clamp" | "data_profile"
99 | "read_mysql" => Type::Table {
100 name: None,
101 columns: None,
102 },
103 "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 "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 "py_import" => Type::PyObject,
126 "py_eval" | "py_call" | "py_getattr" | "py_to_tl" => Type::Any,
127 "py_setattr" => Type::Unit,
128 "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 "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 let fn_ty = infer_expr(function, env);
153 match fn_ty {
154 Type::Function { ret, .. } => *ret,
155 _ => Type::Any,
156 }
157 }
158 }
159
160 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 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 Expr::Member { object, field } => {
182 let obj_ty = infer_expr(object, env);
183 infer_member_access(&obj_ty, field, env)
184 }
185
186 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 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 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 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 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 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 Expr::Yield(_) => Type::Any,
259
260 Expr::Range { .. } => Type::List(Box::new(Type::Int)),
262
263 Expr::Pipe { right, .. } => infer_expr(right, env),
265
266 Expr::Block { expr, .. } => {
268 if let Some(e) = expr {
269 infer_expr(e, env)
270 } else {
271 Type::Unit
272 }
273 }
274
275 Expr::Assign { value, .. } => infer_expr(value, env),
277
278 Expr::StructInit { name, .. } => Type::Struct(name.clone()),
280
281 Expr::EnumVariant { enum_name, .. } => Type::Enum(enum_name.clone()),
283
284 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
297fn 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
349fn 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 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 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 (Type::Decimal, Type::Decimal)
421 | (Type::Decimal, Type::Int)
422 | (Type::Int, Type::Decimal) => Type::Decimal,
423 (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 BinOp::Eq | BinOp::Neq | BinOp::Lt | BinOp::Gt | BinOp::Lte | BinOp::Gte => Type::Bool,
437 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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}