diff --git a/examples/closure.myc b/examples/closure.myc index b44f791..5beb5a4 100644 --- a/examples/closure.myc +++ b/examples/closure.myc @@ -1,5 +1,5 @@ ;; Closure Scope Test -;; Benchmark: 700ns +;; Benchmark: 800ns ;; Output: 15 (do (def make-adder (fn [x] diff --git a/examples/data.myc b/examples/data.myc index c97be19..fc2da33 100644 --- a/examples/data.myc +++ b/examples/data.myc @@ -1,5 +1,5 @@ ;; Complex Data Structure Test -;; Benchmark: 51.8us +;; Benchmark: 51.4us ;; Output: {:input 10, :name "Fibonacci", :output 55} (do (def fib (fn [n] diff --git a/examples/macro_hygiene.myc b/examples/macro_hygiene.myc index 07b6faa..a32336f 100644 --- a/examples/macro_hygiene.myc +++ b/examples/macro_hygiene.myc @@ -1,4 +1,4 @@ -;; Benchmark: 300ns +;; Benchmark: 400ns ;; Output: 5 (do (def y 1) diff --git a/examples/macro_power.myc b/examples/macro_power.myc index 0b0b025..c1c85b3 100644 --- a/examples/macro_power.myc +++ b/examples/macro_power.myc @@ -1,4 +1,4 @@ -;; Benchmark: 400ns +;; Benchmark: 300ns ;; Nested Macro and Arithmetic example ;; Output: 81 diff --git a/examples/tak.myc b/examples/tak.myc index 959227e..3619faa 100644 --- a/examples/tak.myc +++ b/examples/tak.myc @@ -2,7 +2,7 @@ ;; heavily recursive, benefits significantly from specialization of integer arithmetic ;; and direct function calls (skipping dynamic dispatch). ;; Output: 5 - ;; Benchmark: 468.7us + ;; Benchmark: 467.7us (do (def tak (fn [x y z] diff --git a/examples/tco_test.myc b/examples/tco_test.myc index 02e98f0..2ac1006 100644 --- a/examples/tco_test.myc +++ b/examples/tco_test.myc @@ -1,4 +1,4 @@ -;; Benchmark: 2.8ms +;; Benchmark: 2.7ms ;; Output: "done" (do (def count_down (fn [n] diff --git a/examples/tuples.myc b/examples/tuples.myc new file mode 100644 index 0000000..eb2dc15 --- /dev/null +++ b/examples/tuples.myc @@ -0,0 +1,19 @@ +;; Benchmark: 1.9us +;; Demonstration of N-dimensional Tuples, Vectors, and Matrices +;; Based on docs/Tupel 1.md +;; +;; This test verifies that the literals are correctly parsed and represented. +;; Type inference is verified via internal compiler unit tests. +;; +;; Output: [[1 3.14 "text"] [10 20 30 40] [[1 2] [3 4]] [[[1 2] [3 4]] [[5 6] [7 8]]] [[1 2] [3 4 5]] {:x 1, :y 0.3}] + +(do + (def tpl [1, 3.14, "text"]) + (def v [10 20 30 40]) + (def mat2d [[1 2], [3 4]]) + (def mat3d [[[1 2] [3 4]] [[5 6] [7 8]]]) + (def mixed [[1 2] [3 4 5]]) + (def rec {:x 1, :y 0.3}) + + [tpl v mat2d mat3d mixed rec] +) diff --git a/src/ast/compiler/macros.rs b/src/ast/compiler/macros.rs index 9f93dc8..f0b5635 100644 --- a/src/ast/compiler/macros.rs +++ b/src/ast/compiler/macros.rs @@ -467,13 +467,12 @@ mod tests { let mut expander = MacroExpander::new(MacroRegistry::new(), SimpleEvaluator); let expanded = expander.expand(untyped).unwrap(); - if let UntypedKind::Block { exprs } = &expanded.kind { - if let UntypedKind::Expansion { expanded: result, call } = &exprs[1].kind { - if let UntypedKind::Def { name, .. } = &result.kind { - assert_eq!(name.context, Some(call.identity.clone())); - assert_eq!(name.name.as_ref(), "y"); - } else { panic!("Expected Def result, got {:?}", result.kind); } - } + if let UntypedKind::Block { exprs } = &expanded.kind + && let UntypedKind::Expansion { expanded: result, call } = &exprs[1].kind { + if let UntypedKind::Def { name, .. } = &result.kind { + assert_eq!(name.context, Some(call.identity.clone())); + assert_eq!(name.name.as_ref(), "y"); + } else { panic!("Expected Def result, got {:?}", result.kind); } } } @@ -490,20 +489,18 @@ mod tests { let mut expander = MacroExpander::new(MacroRegistry::new(), SimpleEvaluator); let expanded = expander.expand(untyped).unwrap(); - if let UntypedKind::Block { exprs } = &expanded.kind { - if let UntypedKind::Expansion { call: _, expanded: result } = &exprs[1].kind { - if let UntypedKind::If { cond, then_br, else_br } = &result.kind { - if let UntypedKind::Identifier(sym) = &cond.kind { - assert_eq!(sym.name.as_ref(), "false"); - } else { panic!("Expected identifier 'false', got {:?}", cond.kind); } - assert!(matches!(then_br.kind, UntypedKind::Nop)); - if let Some(eb) = else_br { - if let UntypedKind::Constant(Value::Int(n)) = &eb.kind { - assert_eq!(*n, 42); - } else { panic!("Expected 42, got {:?}", eb.kind); } - } else { panic!("Else branch missing"); } - } - } + if let UntypedKind::Block { exprs } = &expanded.kind + && let UntypedKind::Expansion { call: _, expanded: result } = &exprs[1].kind + && let UntypedKind::If { cond, then_br, else_br } = &result.kind { + if let UntypedKind::Identifier(sym) = &cond.kind { + assert_eq!(sym.name.as_ref(), "false"); + } else { panic!("Expected identifier 'false', got {:?}", cond.kind); } + assert!(matches!(then_br.kind, UntypedKind::Nop)); + if let Some(eb) = else_br { + if let UntypedKind::Constant(Value::Int(n)) = &eb.kind { + assert_eq!(*n, 42); + } else { panic!("Expected 42, got {:?}", eb.kind); } + } else { panic!("Else branch missing"); } } } @@ -520,16 +517,13 @@ mod tests { let mut expander = MacroExpander::new(MacroRegistry::new(), SimpleEvaluator); let expanded = expander.expand(untyped).unwrap(); - if let UntypedKind::Block { exprs } = &expanded.kind { - if let UntypedKind::Def { value, .. } = &exprs[1].kind { - if let UntypedKind::Expansion { expanded: result, .. } = &value.kind { - if let UntypedKind::Tuple { elements } = &result.kind { - assert_eq!(elements.len(), 5); - if let UntypedKind::Constant(Value::Int(n)) = &elements[0].kind { assert_eq!(*n, 0); } - if let UntypedKind::Constant(Value::Int(n)) = &elements[4].kind { assert_eq!(*n, 4); } - } - } - } + if let UntypedKind::Block { exprs } = &expanded.kind + && let UntypedKind::Def { value, .. } = &exprs[1].kind + && let UntypedKind::Expansion { expanded: result, .. } = &value.kind + && let UntypedKind::Tuple { elements } = &result.kind { + assert_eq!(elements.len(), 5); + if let UntypedKind::Constant(Value::Int(n)) = &elements[0].kind { assert_eq!(*n, 0); } + if let UntypedKind::Constant(Value::Int(n)) = &elements[4].kind { assert_eq!(*n, 4); } } } diff --git a/src/ast/compiler/type_checker.rs b/src/ast/compiler/type_checker.rs index 02216d9..ac1235e 100644 --- a/src/ast/compiler/type_checker.rs +++ b/src/ast/compiler/type_checker.rs @@ -242,16 +242,47 @@ impl TypeChecker { for e in elements { typed_elements.push(self.check_node(e, ctx)?); } - // Stability: Just List(Any) for now - (BoundKind::Tuple { elements: typed_elements }, StaticType::List(Box::new(StaticType::Any))) + + let ty = if typed_elements.is_empty() { + StaticType::Vector(Box::new(StaticType::Any), 0) + } else { + let first_ty = &typed_elements[0].ty; + let all_same = typed_elements.iter().all(|e| e.ty == *first_ty); + + if all_same { + match first_ty { + StaticType::Vector(inner, len) => { + StaticType::Matrix(inner.clone(), vec![typed_elements.len(), *len]) + }, + StaticType::Matrix(inner, shape) => { + let mut new_shape = vec![typed_elements.len()]; + new_shape.extend(shape); + StaticType::Matrix(inner.clone(), new_shape) + }, + _ => StaticType::Vector(Box::new(first_ty.clone()), typed_elements.len()) + } + } else { + StaticType::Tuple(typed_elements.iter().map(|e| e.ty.clone()).collect()) + } + }; + + (BoundKind::Tuple { elements: typed_elements }, ty) }, BoundKind::Map { entries } => { let mut typed_entries = Vec::new(); + let mut fields = std::collections::BTreeMap::new(); for (k, v) in entries { - typed_entries.push((self.check_node(k, ctx)?, self.check_node(v, ctx)?)); + let kt = self.check_node(k, ctx)?; + let vt = self.check_node(v, ctx)?; + + if let BoundKind::Constant(crate::ast::types::Value::Keyword(kw)) = &kt.kind { + fields.insert(*kw, vt.ty.clone()); + } + + typed_entries.push((kt, vt)); } - (BoundKind::Map { entries: typed_entries }, StaticType::Record(Rc::new(std::collections::BTreeMap::new()))) + (BoundKind::Map { entries: typed_entries }, StaticType::Record(Rc::new(fields))) }, BoundKind::Expansion { original_call, bound_expanded } => { @@ -371,4 +402,54 @@ mod tests { // DateTime comparison -> Bool assert_eq!(get_ret_type(&check_source("(> (date \"2023-01-02\") (date \"2023-01-01\"))")), StaticType::Bool); } + + #[test] + fn test_inference_tuple_vector_matrix() { + // Heterogeneous -> Tuple + assert_eq!( + get_ret_type(&check_source("[1 3.14 \"text\"]")), + StaticType::Tuple(vec![StaticType::Int, StaticType::Float, StaticType::Text]) + ); + + // Homogeneous -> Vector + assert_eq!( + get_ret_type(&check_source("[10 20 30]")), + StaticType::Vector(Box::new(StaticType::Int), 3) + ); + + // Nested Homogeneous -> Matrix + assert_eq!( + get_ret_type(&check_source("[[1 2] [3 4]]")), + StaticType::Matrix(Box::new(StaticType::Int), vec![2, 2]) + ); + + // Deep Matrix + assert_eq!( + get_ret_type(&check_source("[[[1 2]] [[3 4]]]")), + StaticType::Matrix(Box::new(StaticType::Int), vec![2, 1, 2]) + ); + + // Shape mismatch -> Tuple of Vectors + let mixed = get_ret_type(&check_source("[[1 2] [3 4 5]]")); + if let StaticType::Tuple(elements) = mixed { + assert_eq!(elements[0], StaticType::Vector(Box::new(StaticType::Int), 2)); + assert_eq!(elements[1], StaticType::Vector(Box::new(StaticType::Int), 3)); + } else { + panic!("Expected Tuple for shape mismatch, got {:?}", mixed); + } + } + + #[test] + fn test_inference_record() { + use crate::ast::types::Keyword; + let typed = check_source("{:x 1, :y 0.3}"); + let ty = get_ret_type(&typed); + if let StaticType::Record(fields) = ty { + assert_eq!(fields.len(), 2); + assert_eq!(fields.get(&Keyword::intern("x")), Some(&StaticType::Int)); + assert_eq!(fields.get(&Keyword::intern("y")), Some(&StaticType::Float)); + } else { + panic!("Expected Record, got {:?}", ty); + } + } } diff --git a/src/ast/lexer.rs b/src/ast/lexer.rs index 49f5000..23238b7 100644 --- a/src/ast/lexer.rs +++ b/src/ast/lexer.rs @@ -102,12 +102,11 @@ impl<'a> Lexer<'a> { fn skip_whitespace(&mut self) { while let Some(&c) = self.peek() { - if c.is_whitespace() || is_invisible(c) { + if c.is_whitespace() || is_invisible(c) || c == ',' { if c == '\n' { self.line += 1; self.col = 1; - } else if !is_invisible(c) { - // Only increment column for characters that actually take space + } else { self.col += 1; } self.input.next(); diff --git a/src/ast/types.rs b/src/ast/types.rs index 55695e7..d9783ca 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -89,7 +89,10 @@ pub enum StaticType { DateTime, Text, Keyword, - List(Box), + List(Box), // Legacy / Dynamic list + Tuple(Vec), // Heterogeneous fixed-size + Vector(Box, usize), // Homogeneous fixed-size + Matrix(Box, Vec), // Multi-dimensional homogeneous Record(Rc>), Function(Box), FunctionOverloads(Vec), @@ -108,6 +111,23 @@ impl fmt::Display for StaticType { StaticType::Text => write!(f, "text"), StaticType::Keyword => write!(f, "keyword"), StaticType::List(inner) => write!(f, "[{}]", inner), + StaticType::Tuple(elements) => { + write!(f, "[")?; + for (i, el) in elements.iter().enumerate() { + if i > 0 { write!(f, " ")?; } + write!(f, "{}", el)?; + } + write!(f, "]") + }, + StaticType::Vector(inner, len) => write!(f, "vector<{}, {}>", inner, len), + StaticType::Matrix(inner, shape) => { + write!(f, "matrix<{}, [", inner)?; + for (i, s) in shape.iter().enumerate() { + if i > 0 { write!(f, " ")?; } + write!(f, "{}", s)?; + } + write!(f, "]>") + }, StaticType::Record(fields) => { write!(f, "{{")?; for (i, (k, v)) in fields.iter().enumerate() { @@ -138,8 +158,26 @@ impl StaticType { if self == other || matches!(self, StaticType::Any) || matches!(other, StaticType::Any) { return true; } - // Future: Numeric promotion (Int -> Float) - false + + match (self, other) { + // A Vector is a Tuple + (StaticType::Tuple(elements), StaticType::Vector(inner, len)) => { + if elements.len() != *len { return false; } + elements.iter().all(|e| e.is_assignable_from(inner)) + }, + // A Matrix is a Vector (of Vectors/Matrices) + (StaticType::Vector(inner, len), StaticType::Matrix(m_inner, shape)) => { + if shape.is_empty() || shape[0] != *len { return false; } + if shape.len() == 1 { + inner.is_assignable_from(m_inner) + } else { + // It's a matrix of higher dimension, so inner must be assignable from a sub-matrix + let sub_shape = shape[1..].to_vec(); + inner.is_assignable_from(&StaticType::Matrix(m_inner.clone(), sub_shape)) + } + }, + _ => false + } } /// Tries to resolve a call with the given argument types and returns the return type. @@ -168,6 +206,17 @@ impl StaticType { } expected.iter().zip(actual).all(|(e, a)| e.is_assignable_from(a)) } + + /// Returns true if this type and all its recursive elements are scalars (Int, Float, etc.) + pub fn is_scalar_pure(&self) -> bool { + match self { + StaticType::Int | StaticType::Float | StaticType::Bool | StaticType::DateTime => true, + StaticType::Tuple(elements) => elements.iter().all(|e| e.is_scalar_pure()), + StaticType::Vector(inner, _) => inner.is_scalar_pure(), + StaticType::Matrix(inner, _) => inner.is_scalar_pure(), + _ => false, + } + } } impl Value { @@ -189,8 +238,41 @@ impl Value { Value::DateTime(_) => StaticType::DateTime, Value::Text(_) => StaticType::Text, Value::Keyword(_) => StaticType::Keyword, - Value::List(_) => StaticType::List(Box::new(StaticType::Any)), - Value::Record(_) => StaticType::Record(Rc::new(BTreeMap::new())), // Empty for now + Value::List(l) => { + if l.is_empty() { + return StaticType::Vector(Box::new(StaticType::Any), 0); + } + + let element_types: Vec<_> = l.iter().map(|v| v.static_type()).collect(); + + // Check for Homogeneity (Vector) + let first_ty = &element_types[0]; + let all_same = element_types.iter().all(|t| t == first_ty); + + if all_same { + match first_ty { + StaticType::Vector(inner, len) => { + // Possible Matrix + StaticType::Matrix(inner.clone(), vec![l.len(), *len]) + }, + StaticType::Matrix(inner, shape) => { + let mut new_shape = vec![l.len()]; + new_shape.extend(shape); + StaticType::Matrix(inner.clone(), new_shape) + }, + _ => StaticType::Vector(Box::new(first_ty.clone()), l.len()) + } + } else { + StaticType::Tuple(element_types) + } + }, + Value::Record(r) => { + let mut fields = BTreeMap::new(); + for (k, v) in r.iter() { + fields.insert(*k, v.static_type()); + } + StaticType::Record(Rc::new(fields)) + }, Value::Function(_) => StaticType::Any, // Dynamic function Value::Object(o) => StaticType::Object(o.type_name()), Value::Cell(c) => c.borrow().static_type(), diff --git a/src/integration_test.rs b/src/integration_test.rs index a71ba30..cc514f3 100644 --- a/src/integration_test.rs +++ b/src/integration_test.rs @@ -87,7 +87,7 @@ mod tests { for entry in entries { let entry = entry.expect("Invalid entry"); let path = entry.path(); - if path.extension().map_or(false, |ext| ext == "myc") { + if path.extension().is_some_and(|ext| ext == "myc") { let content = fs::read_to_string(&path).expect("Could not read file"); // Find expected output tag: ;; Output: