diff --git a/src/ast/rtl/core.rs b/src/ast/rtl/core.rs index 2483815..2760e18 100644 --- a/src/ast/rtl/core.rs +++ b/src/ast/rtl/core.rs @@ -381,6 +381,11 @@ fn register_comparison(env: &Environment) { (Value::Record(la, va), Value::Record(lb, vb)) => { Value::Bool(std::sync::Arc::ptr_eq(la, lb) && va == vb) } + (Value::Tuple(a), Value::Tuple(b)) => { + Value::Bool(a.len() == b.len() + && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))) + } + (Value::FieldAccessor(a), Value::FieldAccessor(b)) => Value::Bool(a == b), (Value::Void, Value::Void) => Value::Bool(true), _ => Value::Bool(false), } @@ -401,6 +406,11 @@ fn register_comparison(env: &Environment) { (Value::Bool(a), Value::Bool(b)) => Value::Bool(a != b), (Value::Keyword(a), Value::Keyword(b)) => Value::Bool(a != b), (Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a != b), + (Value::Tuple(a), Value::Tuple(b)) => { + Value::Bool(!(a.len() == b.len() + && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y)))) + } + (Value::FieldAccessor(a), Value::FieldAccessor(b)) => Value::Bool(a != b), (Value::Void, Value::Void) => Value::Bool(false), _ => Value::Bool(true), } @@ -605,24 +615,40 @@ fn register_testing(env: &Environment) { })); env.register_native_fn("type-of", type_of_ty, Purity::Pure, |args| { let name = match args.first() { - Some(v) => match v.static_type() { - StaticType::Void => "void", - StaticType::Bool => "bool", - StaticType::Int => "int", - StaticType::Float => "float", - StaticType::DateTime => "datetime", - StaticType::Text => "text", - StaticType::Keyword => "keyword", - StaticType::Tuple(_) - | StaticType::Vector(_, _) - | StaticType::Matrix(_, _) => "tuple", - StaticType::Record(_) => "record", - StaticType::Series(_) => "series", - StaticType::Stream(_) => "stream", - StaticType::Function(_) | StaticType::FunctionOverloads(_) => "function", - StaticType::FieldAccessor(_) => "field-accessor", - StaticType::Object(name) => name, - _ => "unknown", + // Match on Value directly to avoid lossy static_type() conversion + // (e.g. Value::Function → StaticType::Any loses the "function" info). + Some(v) => match v { + Value::Void => "void", + Value::Bool(_) => "bool", + Value::Int(_) => "int", + Value::Float(_) => "float", + Value::DateTime(_) => "datetime", + Value::Text(_) => "text", + Value::Keyword(_) => "keyword", + Value::Tuple(_) => "tuple", + Value::Record(_, _) => "record", + Value::FieldAccessor(_) => "field-accessor", + Value::Function(_) | Value::Closure(_) => "function", + Value::Quote(_) => "quote", + Value::Series(_) => "series", + Value::Stream(_) => "stream", + Value::Cell(c) => { + // Recurse through Cell to get the inner type. + // We can't recurse here directly, so use static_type fallback. + match c.borrow().static_type() { + StaticType::Void => "void", + StaticType::Bool => "bool", + StaticType::Int => "int", + StaticType::Float => "float", + StaticType::DateTime => "datetime", + StaticType::Text => "text", + StaticType::Keyword => "keyword", + StaticType::Record(_) => "record", + StaticType::Series(_) => "series", + StaticType::Stream(_) => "stream", + _ => "unknown", + } + } }, None => panic!("type-of requires 1 argument"), }; diff --git a/tests/rtl.rs b/tests/rtl.rs index 94dd7fd..9710b19 100644 --- a/tests/rtl.rs +++ b/tests/rtl.rs @@ -138,6 +138,103 @@ fn test_environments_from_shared_rtl_are_independent() { assert_eq!(format!("{}", env2.run_script("(+ 1 2)").unwrap()), "3"); } +// --- Bug fixes for wildcard matches --- + +/// `type-of` must return :function for native functions (e.g. `print`). +/// Previously returned :unknown because Value::Function.static_type() yields Any, +/// and the type-of match did not handle Any. +#[test] +fn test_type_of_native_function() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(type-of print)").unwrap()), + ":function", + ); +} + +/// `type-of` must return :function for closures, not :closure. +/// Closures are functions from the user's perspective. +#[test] +fn test_type_of_closure() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(type-of (fn [x] x))").unwrap()), + ":function", + ); +} + +/// `type-of` must return :field-accessor for field accessors. +#[test] +fn test_type_of_field_accessor() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(type-of .name)").unwrap()), + ":field-accessor", + ); +} + +/// The `=` operator must compare tuples structurally, not return false. +#[test] +fn test_equality_tuples() { + let env = Environment::new(); + // Use def bindings because vector literals in call position get flattened. + assert_eq!( + format!("{}", env.run_script("(do (def a [1 2 3]) (def b [1 2 3]) (= a b))").unwrap()), + "true", + ); + assert_eq!( + format!("{}", env.run_script("(do (def a [1 2 3]) (def b [1 2 4]) (= a b))").unwrap()), + "false", + ); + // Nested tuples + assert_eq!( + format!("{}", env.run_script("(do (def a [[1 2] [3 4]]) (def b [[1 2] [3 4]]) (= a b))").unwrap()), + "true", + ); +} + +/// The `<>` operator must compare tuples structurally. +#[test] +fn test_inequality_tuples() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(do (def a [1 2 3]) (def b [1 2 3]) (<> a b))").unwrap()), + "false", + ); + assert_eq!( + format!("{}", env.run_script("(do (def a [1 2 3]) (def b [1 2 4]) (<> a b))").unwrap()), + "true", + ); +} + +/// The `=` operator must compare field accessors by identity. +#[test] +fn test_equality_field_accessors() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(do (def a .name) (def b .name) (= a b))").unwrap()), + "true", + ); + assert_eq!( + format!("{}", env.run_script("(do (def a .name) (def b .age) (= a b))").unwrap()), + "false", + ); +} + +/// The `<>` operator must compare field accessors by identity. +#[test] +fn test_inequality_field_accessors() { + let env = Environment::new(); + assert_eq!( + format!("{}", env.run_script("(do (def a .name) (def b .age) (<> a b))").unwrap()), + "true", + ); + assert_eq!( + format!("{}", env.run_script("(do (def a .name) (def b .name) (<> a b))").unwrap()), + "false", + ); +} + /// Arithmetic operators with incompatible types (e.g. Float - Record) /// must be caught at compile time, not silently return Void at runtime. #[test]