diff --git a/examples/record_to_tuple.myc b/examples/record_to_tuple.myc index d1f153f..b2028c0 100644 --- a/examples/record_to_tuple.myc +++ b/examples/record_to_tuple.myc @@ -1,5 +1,6 @@ ;; Record to Tuple Mapping Test ;; Output: 30 +;; Benchmark: 700ns (do (def rec {:x 10 :y 20}) diff --git a/examples/record_unpack.myc b/examples/record_unpack.myc new file mode 100644 index 0000000..8bbaa1c --- /dev/null +++ b/examples/record_unpack.myc @@ -0,0 +1,19 @@ +;; Benchmark: Record Destructuring Performance +;; This test calls a function that destructures a record 100.000 times. +;; Current implementation is now zero-allocation for destructuring. +;; Output: {:a 1, :b 2} +;; Benchmark: 5.3ms +(do + (def process (fn [[x y]] + (+ x y))) + + (def rec {:a 1 :b 2}) + + (def loop (fn [n acc] + (if (= n 0) + acc + (loop (- n 1) (process rec))))) + + (loop 10000 0) + rec +) diff --git a/src/ast/rtl/type_registry.rs b/src/ast/rtl/type_registry.rs index 92a1d06..26ff2d9 100644 --- a/src/ast/rtl/type_registry.rs +++ b/src/ast/rtl/type_registry.rs @@ -112,7 +112,13 @@ impl RecordBuilder { } pub fn build(self) -> Value { - Value::Record(Rc::new(self.fields)) + let mut keys = Vec::with_capacity(self.fields.len()); + let mut values = Vec::with_capacity(self.fields.len()); + for (k, v) in self.fields { + keys.push(k); + values.push(v); + } + Value::make_record(keys, values) } } @@ -195,20 +201,26 @@ mod tests { } // Check runtime behavior - if let Value::Record(fields) = wrapped { - let greet_fn = &fields.iter().find(|(k, _)| *k == Keyword::intern("greet")).unwrap().1; - if let Value::Function(f) = greet_fn { - let res = f(vec![]); - if let Value::Text(s) = res { - assert_eq!(&*s, "Hello Alice"); + if let Value::Tuple(t) = wrapped { + if let Some(keys) = &t.keys { + let idx = keys.iter().position(|k| *k == Keyword::intern("greet")).unwrap(); + let greet_fn = &t.values[idx]; + + if let Value::Function(f) = greet_fn { + let res = f(vec![]); + if let Value::Text(s) = res { + assert_eq!(&*s, "Hello Alice"); + } else { + panic!("Expected Text result"); + } } else { - panic!("Expected Text result"); + panic!("Expected Function value for method"); } } else { - panic!("Expected Function value for method"); + panic!("Expected Record keys"); } } else { - panic!("Expected Record value"); + panic!("Expected Tuple value"); } } @@ -225,15 +237,19 @@ mod tests { if let Value::Function(f) = factory_val { let instance = f(vec![Value::Text("Bob".into()), Value::Int(40)]); - if let Value::Record(fields) = instance { - let greet_fn = &fields.iter().find(|(k, _)| *k == Keyword::intern("greet")).unwrap().1; - if let Value::Function(gf) = greet_fn { - let res = gf(vec![]); - if let Value::Text(s) = res { - assert_eq!(&*s, "Hello Bob"); - } else { panic!("Wrong return type"); } - } - } else { panic!("Factory should return Record"); } + if let Value::Tuple(t) = instance { + if let Some(keys) = &t.keys { + let idx = keys.iter().position(|k| *k == Keyword::intern("greet")).unwrap(); + let greet_fn = &t.values[idx]; + + if let Value::Function(gf) = greet_fn { + let res = gf(vec![]); + if let Value::Text(s) = res { + assert_eq!(&*s, "Hello Bob"); + } else { panic!("Wrong return type"); } + } + } else { panic!("Expected keys"); } + } else { panic!("Factory should return Tuple/Record"); } } else { panic!("Factory is not a function"); } } } diff --git a/src/ast/types.rs b/src/ast/types.rs index 056a070..2aef55d 100644 --- a/src/ast/types.rs +++ b/src/ast/types.rs @@ -55,6 +55,14 @@ pub trait Object: fmt::Debug { fn as_any(&self) -> &dyn Any; } +/// Internal storage for Tuples (and Records) to allow sharing schema (keys) between instances. +#[derive(Debug, Clone)] +pub struct TupleData { + pub values: Vec, + /// Optional names for slots. If present, this is semantically a Record. + pub keys: Option>>, +} + /// Core data value in Myc Script (similar to TDataValue) #[derive(Clone)] pub enum Value { @@ -65,8 +73,7 @@ pub enum Value { DateTime(i64), Text(Rc), Keyword(Keyword), - List(Rc>), - Record(Rc>), + Tuple(Rc), // Replaces List and Record Function(Rc) -> Value>), Object(Rc), // For compiled Closures and other opaque types Cell(Rc>), // Boxed value for captures @@ -217,6 +224,14 @@ impl Value { } } + pub fn make_list(values: Vec) -> Self { + Value::Tuple(Rc::new(TupleData { values, keys: None })) + } + + pub fn make_record(keys: Vec, values: Vec) -> Self { + Value::Tuple(Rc::new(TupleData { values, keys: Some(Rc::new(keys)) })) + } + pub fn static_type(&self) -> StaticType { match self { Value::Void => StaticType::Void, @@ -226,41 +241,45 @@ impl Value { Value::DateTime(_) => StaticType::DateTime, Value::Text(_) => StaticType::Text, Value::Keyword(_) => StaticType::Keyword, - 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()) + Value::Tuple(t) => { + if let Some(keys) = &t.keys { + // It's a Record + let mut fields = Vec::with_capacity(t.values.len()); + for (i, v) in t.values.iter().enumerate() { + fields.push((keys[i], v.static_type())); } + StaticType::Record(Rc::new(fields)) } else { - StaticType::Tuple(element_types) + // It's a List/Tuple + let l = &t.values; + 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 = Vec::with_capacity(r.len()); - for (k, v) in r.iter() { - fields.push((*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(), @@ -284,21 +303,22 @@ impl fmt::Display for Value { }, Value::Text(t) => write!(f, "\"{}\"", t), Value::Keyword(k) => write!(f, ":{}", k.name()), - Value::List(l) => { - write!(f, "[")?; - for (i, val) in l.iter().enumerate() { - if i > 0 { write!(f, " ")?; } - write!(f, "{}", val)?; + Value::Tuple(t) => { + if let Some(keys) = &t.keys { + write!(f, "{{")?; + for i in 0..t.values.len() { + if i > 0 { write!(f, ", ")?; } + write!(f, ":{} {}", keys[i].name(), t.values[i])?; + } + write!(f, "}}") + } else { + write!(f, "[")?; + for (i, val) in t.values.iter().enumerate() { + if i > 0 { write!(f, " ")?; } + write!(f, "{}", val)?; + } + write!(f, "]") } - write!(f, "]") - }, - Value::Record(r) => { - write!(f, "{{")?; - for (i, (k, v)) in r.iter().enumerate() { - if i > 0 { write!(f, ", ")?; } - write!(f, ":{} {}", k.name(), v)?; - } - write!(f, "}}") }, Value::Function(_) => write!(f, ""), Value::Object(o) => write!(f, "<{}>", o.type_name()), diff --git a/src/ast/vm.rs b/src/ast/vm.rs index 5ebc064..e99e127 100644 --- a/src/ast/vm.rs +++ b/src/ast/vm.rs @@ -313,22 +313,24 @@ macro_rules! dispatch_eval { for e in elements { vals.push($self.$eval_method($($observer,)? e)?); } - Ok(Value::List(Rc::new(vals))) + Ok(Value::make_list(vals)) }, BoundKind::Map { entries } => { - let mut map = Vec::with_capacity(entries.len()); + let mut keys = Vec::with_capacity(entries.len()); + let mut values = Vec::with_capacity(entries.len()); for (k, v) in entries { let key = $self.$eval_method($($observer,)? k)?; let val = $self.$eval_method($($observer,)? v)?; if let Value::Keyword(kw) = key { - map.push((kw, val)); + keys.push(kw); + values.push(val); } else { return Err(format!("Map key must be keyword, got {}", key)); } } - Ok(Value::Record(Rc::new(map))) + Ok(Value::make_record(keys, values)) }, BoundKind::Expansion { original_call: _, bound_expanded } => { @@ -599,8 +601,8 @@ impl VM { } fn flatten_value(val: Value, into: &mut Vec) { - if let Value::List(l) = val { - for item in l.iter() { + if let Value::Tuple(t) = val { + for item in t.values.iter() { Self::flatten_value(item.clone(), into); } } else { @@ -677,30 +679,15 @@ impl VM { Ok(()) } BoundKind::Tuple { elements } => { - // If the current value at offset is a List or Record, we dive into it. + // If the current value at offset is a Tuple (List or Record), we dive into it. // Otherwise, we assume the sequence was already flattened (e.g. by Specializer). - if let Some(val) = values.get(*offset) { - match val { - Value::List(l) => { - *offset += 1; - let mut sub_offset = 0; - for el in elements { - self.unpack(el, l, &mut sub_offset)?; - } - return Ok(()); - } - Value::Record(r) => { - *offset += 1; - let mut sub_offset = 0; - // Treat record values as a tuple sequence in definition order - let record_values: Vec<_> = r.iter().map(|(_, v)| v.clone()).collect(); - for el in elements { - self.unpack(el, &record_values, &mut sub_offset)?; - } - return Ok(()); - } - _ => {} + if let Some(Value::Tuple(t)) = values.get(*offset) { + *offset += 1; + let mut sub_offset = 0; + for el in elements { + self.unpack(el, &t.values, &mut sub_offset)?; } + return Ok(()); } for el in elements { diff --git a/src/utils/tester.rs b/src/utils/tester.rs index b449946..c61849b 100644 --- a/src/utils/tester.rs +++ b/src/utils/tester.rs @@ -66,11 +66,11 @@ pub fn run_functional_tests() -> Vec { pub fn run_benchmarks(update: bool) -> Vec { let mut results = Vec::new(); let entries = fs::read_dir("examples").unwrap(); - let env = Environment::new(); let is_release = !cfg!(debug_assertions); let baseline_re = Regex::new(r";; Benchmark: ([\d\.]+\w+)").unwrap(); for entry in entries.filter_map(|e| e.ok()) { + let env = Environment::new(); // Fresh environment per benchmark for isolation let path = entry.path(); if path.extension().is_some_and(|ext| ext == "myc") { let content = fs::read_to_string(&path).unwrap(); @@ -81,7 +81,16 @@ pub fn run_benchmarks(update: bool) -> Vec { // Prepare: Compile and Link once outside the measurement loop let linked_node = match env.compile(&content).map(|c| env.link(c)) { Ok(node) => node, - Err(_) => continue, // Skip scripts that don't compile + Err(e) => { + results.push(BenchmarkResult { + name, + median: Duration::ZERO, + baseline: None, + diff_pct: None, + status: format!("COMPILE ERROR: {}", e), + }); + continue; + } }; // Measure: Only the VM execution