use myc::ast::environment::Environment; use myc::ast::types::Value; // ── Stage B: HM Let-Polymorphism ───────────────────────────────────────────── // // These tests verify that user-defined generic functions (e.g. `last`, `prev`) // work correctly across multiple series element types without type conflicts. // // Before Stage B: TypeVars are unified once per call site. The optimizer // hides failures by inlining, but diagnostics may report false conflicts and // return types remain `Any` instead of the precise inferred type. // // After Stage B: `generalize` wraps function types in `Forall`; `instantiate` // gives each call site fresh TypeVars. No conflicts, precise return types. // ───────────────────────────────────────────────────────────────────────────── /// A generic accessor function must compile without any type-conflict diagnostics /// when called with two series of different element types. /// /// This is the primary Stage B regression test. Before let-polymorphism, the /// TypeVar from the first call may leak into the second, producing a spurious /// type-conflict error. #[test] fn test_let_poly_no_conflict_diagnostic() { let env = Environment::new(); let source = r#" (do (def last (fn [s] (s 0))) (def a (series 5)) (push a 1.5) (def b (series 5)) (push b "hello") (last a) (last b) ) "#; let result = env.compile(source); assert!( !result.diagnostics.has_errors(), "Generic function used with two series types must not produce type errors: {}", result.diagnostics.format_errors() ); let has_type_conflict = result .diagnostics .items .iter() .any(|d| d.message.contains("type") || d.message.contains("conflict") || d.message.contains("mismatch")); assert!( !has_type_conflict, "No type-conflict diagnostics expected for a polymorphic function: {:?}", result.diagnostics.items ); } /// Runtime correctness: `last` retrieves the correct value from Float and Text series. #[test] fn test_let_poly_two_series_types_runtime() { let env = Environment::new(); let source = r#" (do (def last (fn [s] (s 0))) (def a (series 5)) (push a 1.5) (push a 2.5) (def b (series 5)) (push b "hello") (push b "world") [(last a) (last b)] ) "#; let result = env.run_script(source); match result { Ok(Value::Tuple(elems)) if elems.len() == 2 => { match (&elems[0], &elems[1]) { (Value::Float(f), Value::Text(t)) if (*f - 2.5).abs() < 1e-9 && t.as_ref() == "world" => {} other => panic!("Expected [Float(2.5), Text(\"world\")], got {:?}", other), } } other => panic!("Expected tuple result, got {:?}", other), } } /// A generic function works on record series as well as scalar series. #[test] fn test_let_poly_record_series() { let env = Environment::new(); let source = r#" (do (def last (fn [s] (s 0))) (def prices (series 5)) (push prices {:close 42.0 :vol 100}) (def names (series 5)) (push names {:name "Alice"}) [ ((.close prices) 0) ((.name names) 0) ] ) "#; let result = env.run_script(source); match result { Ok(Value::Tuple(elems)) if elems.len() == 2 => { match (&elems[0], &elems[1]) { (Value::Float(f), Value::Text(t)) if (*f - 42.0).abs() < 1e-9 && t.as_ref() == "Alice" => {} other => panic!("Expected [Float(42.0), Text(\"Alice\")], got {:?}", other), } } other => panic!("Expected tuple result, got {:?}", other), } } /// Value restriction: a series binding must NOT be generalized. /// Pushing incompatible types into the same series must remain a type error. #[test] fn test_let_poly_value_restriction_series() { let env = Environment::new(); let source = r#" (do (def s (series 5)) (push s 1.5) (push s "hello") (s 0) ) "#; let result = env.compile(source); assert!( result.diagnostics.has_errors(), "Pushing incompatible types into the same series must produce a type error" ); } /// Higher-order function: `apply-f` passes a generic function to different series. /// Both calls must compile cleanly and return the correct values. #[test] fn test_let_poly_higher_order() { let env = Environment::new(); let source = r#" (do (def last (fn [s] (s 0))) (def apply-f (fn [f s] (f s))) (def a (series 5)) (push a 1.5) (push a 2.5) (def b (series 5)) (push b "hello") (push b "world") [(apply-f last a) (apply-f last b)] ) "#; let result = env.run_script(source); match result { Ok(Value::Tuple(elems)) if elems.len() == 2 => { match (&elems[0], &elems[1]) { (Value::Float(f), Value::Text(t)) if (*f - 2.5).abs() < 1e-9 && t.as_ref() == "world" => {} other => panic!("Expected [Float(2.5), Text(\"world\")], got {:?}", other), } } other => panic!("Expected tuple result, got {:?}", other), } } /// When a generic function returns `Any` (because its TypeVar is not resolved), /// pushing the result into a new series leaves that series with an unresolved /// element type. Field access on `Series(TypeVar)` then fails because the type /// checker cannot find the record layout. /// /// After Stage B + call-site unification: the return type of `(get-first prices)` /// is correctly inferred as `Record({:price Float})`, the pushed series gets /// `Series(Record({:price Float}))`, and field access works. #[test] fn test_let_poly_generic_return_used_in_field_series() { let env = Environment::new(); let source = r#" (do (def get-first (fn [s] (s 0))) (def prices (series 5)) (push prices {:price 42.0}) (def out (series 3)) (push out (get-first prices)) ((.price out) 0) ) "#; let result = env.run_script(source); match result { Ok(Value::Float(v)) if (v - 42.0).abs() < 1e-9 => {} other => panic!("Expected Float(42.0), got {:?}", other), } }