From a11fed42b71ecf7c26be8b7b9916570be0266cf6 Mon Sep 17 00:00:00 2001 From: Sigilante Date: Sat, 31 Jan 2026 22:45:36 -0600 Subject: [PATCH] Rework memory representations in runtime. --- crates/goth-eval/src/eval.rs | 34 ++-- crates/goth-eval/src/lib.rs | 148 ++++++++++++++++++ crates/goth-eval/src/prim.rs | 49 +++--- crates/goth-eval/src/value.rs | 10 +- .../contracts/closure_reuse_contract.goth | 13 ++ .../contracts/tensor_identity_contract.goth | 11 ++ .../tensor_independence_contract.goth | 10 ++ 7 files changed, 230 insertions(+), 45 deletions(-) create mode 100644 examples/contracts/closure_reuse_contract.goth create mode 100644 examples/contracts/tensor_identity_contract.goth create mode 100644 examples/contracts/tensor_independence_contract.goth diff --git a/crates/goth-eval/src/eval.rs b/crates/goth-eval/src/eval.rs index 26cf80a..c5b415d 100644 --- a/crates/goth-eval/src/eval.rs +++ b/crates/goth-eval/src/eval.rs @@ -116,8 +116,8 @@ impl Evaluator { Expr::Lit(lit) => Ok(self.eval_literal(lit)), Expr::Prim(name) => env.get_global(name).ok_or_else(|| EvalError::not_implemented(format!("primitive: {}", name))), Expr::App(func, arg) => { let func_val = self.eval_with_env(func, env)?; let arg_val = self.eval_with_env(arg, env)?; self.apply(func_val, arg_val) } - Expr::Lam(body) => Ok(Value::Closure(Closure { arity: 1, body: (**body).clone(), env: env.capture(), preconditions: vec![], postconditions: vec![] })), - Expr::LamN(n, body) => Ok(Value::Closure(Closure { arity: *n, body: (**body).clone(), env: env.capture(), preconditions: vec![], postconditions: vec![] })), + Expr::Lam(body) => Ok(Value::Closure(Rc::new(Closure { arity: 1, body: (**body).clone(), env: env.capture(), preconditions: vec![], postconditions: vec![] }))), + Expr::LamN(n, body) => Ok(Value::Closure(Rc::new(Closure { arity: *n, body: (**body).clone(), env: env.capture(), preconditions: vec![], postconditions: vec![] }))), Expr::Let { pattern, type_: _, value, body } => { let val = self.eval_with_env(value, env)?; let mut new_env = env.clone(); self.bind_pattern(pattern, val, &mut new_env)?; self.eval_with_env(body, &new_env) } Expr::LetRec { bindings, body } => { let mut new_env = env.clone(); @@ -135,7 +135,7 @@ impl Evaluator { Expr::Tuple(exprs) => { let values: Vec = exprs.iter().map(|e| self.eval_with_env(e, env)).collect::>()?; Ok(Value::tuple(values)) } Expr::Record(fields) => { let map: HashMap = fields.iter().map(|(name, expr)| { let val = self.eval_with_env(expr, env)?; Ok((name.to_string(), val)) }).collect::>()?; Ok(Value::Record(Rc::new(map))) } Expr::Array(exprs) => { let values: Vec = exprs.iter().map(|e| self.eval_with_env(e, env)).collect::>()?; Ok(self.values_to_tensor(values)) } - Expr::ArrayFill { shape, value } => { let shape_vals: Vec = shape.iter().map(|e| { let v = self.eval_with_env(e, env)?; v.as_int().map(|n| n as usize).ok_or_else(|| EvalError::type_error("Int", &v)) }).collect::>()?; let fill_val = self.eval_with_env(value, env)?; let size: usize = shape_vals.iter().product(); let data = vec![fill_val; size]; Ok(Value::Tensor(Tensor::from_values(shape_vals, data))) } + Expr::ArrayFill { shape, value } => { let shape_vals: Vec = shape.iter().map(|e| { let v = self.eval_with_env(e, env)?; v.as_int().map(|n| n as usize).ok_or_else(|| EvalError::type_error("Int", &v)) }).collect::>()?; let fill_val = self.eval_with_env(value, env)?; let size: usize = shape_vals.iter().product(); let data = vec![fill_val; size]; Ok(Value::Tensor(Rc::new(Tensor::from_values(shape_vals, data)))) } Expr::Variant { constructor, payload } => { let payload_val = match payload { Some(p) => Some(self.eval_with_env(p, env)?), None => None }; Ok(Value::variant(constructor.to_string(), payload_val)) } Expr::Field(base, access) => { let val = self.eval_with_env(base, env)?; self.access_field(val, access) } Expr::Index(base, indices) => { let arr = self.eval_with_env(base, env)?; let idx_vals: Vec = indices.iter().map(|e| { let v = self.eval_with_env(e, env)?; v.as_index().ok_or_else(|| EvalError::type_error("Int", &v)) }).collect::>()?; self.index_value(arr, &idx_vals) } @@ -197,8 +197,9 @@ impl Evaluator { /// Single step of application that may return a tail call for trampolining. fn apply_once(&mut self, func: Value, arg: Value) -> EvalResult { match func { - Value::Closure(closure) => { - if closure.arity == 1 { + Value::Closure(rc_closure) => { + if rc_closure.arity == 1 { + let closure = Rc::unwrap_or_clone(rc_closure); let mut new_env = closure.env.clone(); new_env.push(arg.clone()); @@ -215,15 +216,16 @@ impl Evaluator { Ok(TcoResult::Done(result)) } } else { - let remaining = (closure.arity - 1) as usize; - Ok(TcoResult::Done(Value::Partial { func: Box::new(Value::Closure(closure)), args: vec![arg], remaining })) + let remaining = (rc_closure.arity - 1) as usize; + Ok(TcoResult::Done(Value::Partial { func: Box::new(Value::Closure(rc_closure)), args: vec![arg], remaining })) } } Value::Partial { func, mut args, remaining } => { args.push(arg); if remaining == 1 { match *func { - Value::Closure(closure) => { + Value::Closure(rc_closure) => { + let closure = Rc::unwrap_or_clone(rc_closure); let mut new_env = closure.env.clone(); for a in &args { new_env.push(a.clone()); @@ -392,7 +394,7 @@ impl Evaluator { Pattern::Var(_) => { env.push(val.clone()); Ok(true) } Pattern::Lit(lit) => { let lit_val = self.eval_literal(lit); Ok(val.deep_eq(&lit_val)) } Pattern::Array(pats) => { match val { Value::Tensor(t) => { if t.rank() != 1 || t.len() != pats.len() { return Ok(false); } for (i, pat) in pats.iter().enumerate() { let elem = t.get_flat(i).unwrap(); if !self.match_pattern(pat, &elem, env)? { return Ok(false); } } Ok(true) } _ => Ok(false) } } - Pattern::ArraySplit { head, tail } => { match val { Value::Tensor(t) => { if t.rank() != 1 || t.len() < head.len() { return Ok(false); } for (i, pat) in head.iter().enumerate() { let elem = t.get_flat(i).unwrap(); if !self.match_pattern(pat, &elem, env)? { return Ok(false); } } let tail_data: Vec = (head.len()..t.len()).map(|i| t.get_flat(i).unwrap()).collect(); let tail_tensor = Tensor::from_values(vec![tail_data.len()], tail_data); self.match_pattern(tail, &Value::Tensor(tail_tensor), env) } _ => Ok(false) } } + Pattern::ArraySplit { head, tail } => { match val { Value::Tensor(t) => { if t.rank() != 1 || t.len() < head.len() { return Ok(false); } for (i, pat) in head.iter().enumerate() { let elem = t.get_flat(i).unwrap(); if !self.match_pattern(pat, &elem, env)? { return Ok(false); } } let tail_data: Vec = (head.len()..t.len()).map(|i| t.get_flat(i).unwrap()).collect(); let tail_tensor = Rc::new(Tensor::from_values(vec![tail_data.len()], tail_data)); self.match_pattern(tail, &Value::Tensor(tail_tensor), env) } _ => Ok(false) } } Pattern::Tuple(pats) => { match val { Value::Tuple(vals) => { if vals.len() != pats.len() { return Ok(false); } for (pat, v) in pats.iter().zip(vals) { if !self.match_pattern(pat, v, env)? { return Ok(false); } } Ok(true) } Value::Unit if pats.is_empty() => Ok(true), _ => Ok(false) } } Pattern::Variant { constructor, payload } => { match val { Value::Variant { tag, payload: val_payload } => { if tag.as_str() != constructor.as_ref() { return Ok(false); } match (payload, val_payload) { (None, None) => Ok(true), (Some(pat), Some(v)) => self.match_pattern(pat, v, env), _ => Ok(false) } } _ => Ok(false) } } Pattern::Typed(inner, _ty) => self.match_pattern(inner, val, env), @@ -408,16 +410,16 @@ impl Evaluator { } fn values_to_tensor_shaped(&self, shape: Vec, values: Vec) -> Value { - if values.is_empty() { return Value::Tensor(Tensor::from_ints(vec![])); } + if values.is_empty() { return Value::Tensor(Rc::new(Tensor::from_ints(vec![]))); } let all_int = values.iter().all(|v| matches!(v, Value::Int(_))); let all_float = values.iter().all(|v| matches!(v, Value::Float(_) | Value::Int(_))); let all_bool = values.iter().all(|v| matches!(v, Value::Bool(_))); let all_char = values.iter().all(|v| matches!(v, Value::Char(_))); - if all_int { Value::Tensor(Tensor { shape, data: crate::value::TensorData::Int(values.iter().map(|v| v.as_int().unwrap()).collect()) }) } - else if all_float { Value::Tensor(Tensor { shape, data: crate::value::TensorData::Float(values.iter().map(|v| ordered_float::OrderedFloat(v.coerce_float().unwrap())).collect()) }) } - else if all_bool { Value::Tensor(Tensor { shape, data: crate::value::TensorData::Bool(values.iter().map(|v| v.as_bool().unwrap()).collect()) }) } - else if all_char { Value::Tensor(Tensor { shape, data: crate::value::TensorData::Char(values.iter().map(|v| v.as_char().unwrap()).collect()) }) } - else { Value::Tensor(Tensor::from_values(shape, values)) } + if all_int { Value::Tensor(Rc::new(Tensor { shape, data: crate::value::TensorData::Int(values.iter().map(|v| v.as_int().unwrap()).collect()) })) } + else if all_float { Value::Tensor(Rc::new(Tensor { shape, data: crate::value::TensorData::Float(values.iter().map(|v| ordered_float::OrderedFloat(v.coerce_float().unwrap())).collect()) })) } + else if all_bool { Value::Tensor(Rc::new(Tensor { shape, data: crate::value::TensorData::Bool(values.iter().map(|v| v.as_bool().unwrap()).collect()) })) } + else if all_char { Value::Tensor(Rc::new(Tensor { shape, data: crate::value::TensorData::Char(values.iter().map(|v| v.as_char().unwrap()).collect()) })) } + else { Value::Tensor(Rc::new(Tensor::from_values(shape, values))) } } fn access_field(&self, val: Value, access: &FieldAccess) -> EvalResult { @@ -513,7 +515,7 @@ impl Evaluator { let body = Expr::App(Box::new(Expr::Idx(2)), Box::new(Expr::App(Box::new(Expr::Idx(1)), Box::new(Expr::Idx(0))))); let mut env = Env::with_globals(Rc::clone(&self.globals)); env.push(f); env.push(g); // Push f first, then g, so g is at Idx(1) and f is at Idx(2) - Ok(Value::Closure(Closure { arity: 1, body, env, preconditions: vec![], postconditions: vec![] })) + Ok(Value::Closure(Rc::new(Closure { arity: 1, body, env, preconditions: vec![], postconditions: vec![] }))) } fn eval_write(&mut self, content: Value, target: Value) -> EvalResult { diff --git a/crates/goth-eval/src/lib.rs b/crates/goth-eval/src/lib.rs index c37fc27..5dd9030 100644 --- a/crates/goth-eval/src/lib.rs +++ b/crates/goth-eval/src/lib.rs @@ -15,6 +15,7 @@ pub mod prelude { mod tests { use super::prelude::*; use goth_ast::prelude::*; + use std::rc::Rc; #[test] fn test_int_literal() { assert_eq!(eval(&Expr::int(42)).unwrap(), Value::Int(42)); } #[test] fn test_float_literal() { assert_eq!(eval(&Expr::float(3.14)).unwrap(), Value::float(3.14)); } @@ -1058,4 +1059,151 @@ mod tests { } let _ = fs::remove_file(temp_path); } + + // ============ Rc-wrapping invariant tests ============ + // These must pass BEFORE and AFTER the Rc-wrapping refactor. + + #[test] + fn invariant_tensor_string_roundtrip() { + let v = Value::string("hello"); + match &v { + Value::Tensor(t) => assert_eq!(t.to_string_value(), Some("hello".to_string())), + _ => panic!("Expected Tensor"), + } + } + + #[test] + fn invariant_tensor_clone_independence() { + let v1 = Value::Tensor(Rc::new(Tensor::from_ints(vec![1, 2, 3]))); + let v2 = v1.clone(); + assert_eq!(v1, v2); + } + + #[test] + fn invariant_tensor_deep_eq() { + let a = Value::Tensor(Rc::new(Tensor::from_ints(vec![1, 2, 3]))); + let b = Value::Tensor(Rc::new(Tensor::from_ints(vec![1, 2, 3]))); + assert!(a.deep_eq(&b)); + } + + #[test] + fn invariant_tensor_as_tensor() { + let v = Value::Tensor(Rc::new(Tensor::from_floats(vec![1.0, 2.0]))); + let t = v.as_tensor().unwrap(); + assert_eq!(t.shape, vec![2]); + assert_eq!(t.len(), 2); + } + + #[test] + fn invariant_tensor_display() { + let v = Value::Tensor(Rc::new(Tensor::from_ints(vec![1, 2, 3]))); + let s = format!("{}", v); + assert!(s.contains("1") && s.contains("2") && s.contains("3")); + } + + #[test] + fn invariant_concat_strings() { + let mut e = Evaluator::new(); + let expr = Expr::app( + Expr::app(Expr::name("concat"), Expr::Lit(Literal::String("ab".into()))), + Expr::Lit(Literal::String("cd".into())), + ); + let result = e.eval(&expr).unwrap(); + match &result { + Value::Tensor(t) => assert_eq!(t.to_string_value(), Some("abcd".to_string())), + _ => panic!("Expected string tensor"), + } + } + + #[test] + fn invariant_concat_int_arrays() { + let mut e = Evaluator::new(); + let expr = Expr::app( + Expr::app( + Expr::name("concat"), + Expr::array(vec![Expr::int(1), Expr::int(2)]), + ), + Expr::array(vec![Expr::int(3), Expr::int(4)]), + ); + let result = e.eval(&expr).unwrap(); + match &result { + Value::Tensor(t) => { + assert_eq!(t.len(), 4); + assert_eq!(t.get_flat(0), Some(Value::Int(1))); + assert_eq!(t.get_flat(3), Some(Value::Int(4))); + } + _ => panic!("Expected tensor"), + } + } + + #[test] + fn invariant_closure_apply() { + let result = eval(&Expr::app( + Expr::lam(Expr::mul(Expr::idx(0), Expr::int(2))), + Expr::int(21), + )).unwrap(); + assert_eq!(result, Value::Int(42)); + } + + #[test] + fn invariant_closure_partial_application() { + let add_fn = Expr::lam(Expr::lam(Expr::add(Expr::idx(1), Expr::idx(0)))); + let add5 = Expr::app(add_fn, Expr::int(5)); + let result = eval(&Expr::app(add5, Expr::int(3))).unwrap(); + assert_eq!(result, Value::Int(8)); + } + + #[test] + fn invariant_closure_captures_env() { + let expr = Expr::let_( + Pattern::var("x"), Expr::int(10), + Expr::app(Expr::lam(Expr::add(Expr::idx(0), Expr::idx(1))), Expr::int(5)), + ); + assert_eq!(eval(&expr).unwrap(), Value::Int(15)); + } + + #[test] + fn invariant_closure_eq() { + let c1 = Value::closure(1, Expr::idx(0), Env::new()); + let c2 = Value::closure(1, Expr::idx(0), Env::new()); + assert_eq!(c1, c2); + } + + #[test] + fn invariant_closure_is_callable() { + let v = Value::closure(1, Expr::idx(0), Env::new()); + assert!(v.is_callable()); + } + + // ============ Rc sharing tests ============ + + #[test] + fn sharing_closure_clone_shares_rc() { + let v1 = Value::closure(1, Expr::idx(0), Env::new()); + let v2 = v1.clone(); + match (&v1, &v2) { + (Value::Closure(a), Value::Closure(b)) => assert!(Rc::ptr_eq(a, b)), + _ => panic!("Expected closures"), + } + } + + #[test] + fn sharing_tensor_clone_shares_rc() { + let v1 = Value::Tensor(Rc::new(Tensor::from_ints(vec![1, 2, 3]))); + let v2 = v1.clone(); + match (&v1, &v2) { + (Value::Tensor(a), Value::Tensor(b)) => assert!(Rc::ptr_eq(a, b)), + _ => panic!("Expected tensors"), + } + } + + #[test] + fn sharing_string_value_is_rc_tensor() { + let v1 = Value::string("hello"); + let v2 = v1.clone(); + match (&v1, &v2) { + (Value::Tensor(a), Value::Tensor(b)) => assert!(Rc::ptr_eq(a, b)), + _ => panic!("Expected tensors"), + } + } } diff --git a/crates/goth-eval/src/prim.rs b/crates/goth-eval/src/prim.rs index 863445a..71cf80c 100644 --- a/crates/goth-eval/src/prim.rs +++ b/crates/goth-eval/src/prim.rs @@ -1,5 +1,6 @@ //! Primitive operations for Goth +use std::rc::Rc; use crate::value::{Value, Tensor, PrimFn}; use crate::error::{EvalError, EvalResult}; use ordered_float::OrderedFloat; @@ -293,7 +294,7 @@ pub fn apply_prim(prim: PrimFn, args: Vec) -> EvalResult { file.read_exact(&mut buf) .map_err(|e| EvalError::IoError(format!("Failed to read {} bytes from '{}': {}", count, path, e)))?; let byte_vals: Vec = buf.iter().map(|&b| b as i128).collect(); - Ok(Value::Tensor(Tensor::from_ints(byte_vals))) + Ok(Value::Tensor(Rc::new(Tensor::from_ints(byte_vals)))) } PrimFn::WriteBytes => { if args.len() != 2 { return Err(EvalError::ArityMismatch { expected: 2, got: args.len() }); } @@ -373,10 +374,10 @@ fn add(left: Value, right: Value) -> EvalResult { (Value::Tensor(a), Value::Tensor(b)) => { if a.shape != b.shape { return Err(EvalError::shape_mismatch(format!("Cannot add tensors with shapes {:?} and {:?}", a.shape, b.shape))); } let result = a.zip_with(b, |x, y| add(x, y).unwrap_or(Value::Error("add failed".into()))).ok_or_else(|| EvalError::shape_mismatch("zip failed"))?; - Ok(Value::Tensor(result)) + Ok(Value::Tensor(Rc::new(result))) } - (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(t.map(|x| add(x, scalar.clone()).unwrap_or(Value::Error("add failed".into()))))), - (scalar, Value::Tensor(t)) if scalar.is_numeric() => Ok(Value::Tensor(t.map(|x| add(scalar.clone(), x).unwrap_or(Value::Error("add failed".into()))))), + (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(Rc::new(t.map(|x| add(x, scalar.clone()).unwrap_or(Value::Error("add failed".into())))))), + (scalar, Value::Tensor(t)) if scalar.is_numeric() => Ok(Value::Tensor(Rc::new(t.map(|x| add(scalar.clone(), x).unwrap_or(Value::Error("add failed".into())))))), _ => Err(EvalError::type_error_msg(format!("Cannot add {} and {}", left.type_name(), right.type_name()))), } } @@ -405,7 +406,7 @@ fn sub(left: Value, right: Value) -> EvalResult { (Value::Tensor(a), Value::Tensor(b)) => { if a.shape != b.shape { return Err(EvalError::shape_mismatch("Tensor shapes must match for subtraction")); } let result = a.zip_with(b, |x, y| sub(x, y).unwrap_or(Value::Error("sub failed".into()))).ok_or_else(|| EvalError::shape_mismatch("zip failed"))?; - Ok(Value::Tensor(result)) + Ok(Value::Tensor(Rc::new(result))) } _ => Err(EvalError::type_error_msg(format!("Cannot subtract {} and {}", left.type_name(), right.type_name()))), } @@ -435,10 +436,10 @@ fn mul(left: Value, right: Value) -> EvalResult { (Value::Tensor(a), Value::Tensor(b)) => { if a.shape != b.shape { return Err(EvalError::shape_mismatch("Tensor shapes must match for multiplication")); } let result = a.zip_with(b, |x, y| mul(x, y).unwrap_or(Value::Error("mul failed".into()))).ok_or_else(|| EvalError::shape_mismatch("zip failed"))?; - Ok(Value::Tensor(result)) + Ok(Value::Tensor(Rc::new(result))) } - (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(t.map(|x| mul(x, scalar.clone()).unwrap_or(Value::Error("mul failed".into()))))), - (scalar, Value::Tensor(t)) if scalar.is_numeric() => Ok(Value::Tensor(t.map(|x| mul(scalar.clone(), x).unwrap_or(Value::Error("mul failed".into()))))), + (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(Rc::new(t.map(|x| mul(x, scalar.clone()).unwrap_or(Value::Error("mul failed".into())))))), + (scalar, Value::Tensor(t)) if scalar.is_numeric() => Ok(Value::Tensor(Rc::new(t.map(|x| mul(scalar.clone(), x).unwrap_or(Value::Error("mul failed".into())))))), _ => Err(EvalError::type_error_msg(format!("Cannot multiply {} and {}", left.type_name(), right.type_name()))), } } @@ -471,7 +472,7 @@ fn div(left: Value, right: Value) -> EvalResult { (Value::Float(a), Value::Float(b)) => if b.0 == 0.0 { Err(EvalError::DivisionByZero) } else { Ok(Value::Float(OrderedFloat(a.0 / b.0))) }, (Value::Int(a), Value::Float(b)) => if b.0 == 0.0 { Err(EvalError::DivisionByZero) } else { Ok(Value::Float(OrderedFloat(*a as f64 / b.0))) }, (Value::Float(a), Value::Int(b)) => if *b == 0 { Err(EvalError::DivisionByZero) } else { Ok(Value::Float(OrderedFloat(a.0 / *b as f64))) }, - (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(t.map(|x| div(x, scalar.clone()).unwrap_or(Value::Error("div failed".into()))))), + (Value::Tensor(t), scalar) if scalar.is_numeric() => Ok(Value::Tensor(Rc::new(t.map(|x| div(x, scalar.clone()).unwrap_or(Value::Error("div failed".into())))))), _ => Err(EvalError::type_error_msg(format!("Cannot divide {} by {}", left.type_name(), right.type_name()))), } } @@ -590,7 +591,7 @@ fn negate(value: Value) -> EvalResult { } Value::Int(n) => n.checked_neg().map(Value::Int).ok_or_else(|| EvalError::Overflow(format!("negation of {} overflows", n))), Value::Float(f) => Ok(Value::Float(OrderedFloat(-f.0))), - Value::Tensor(t) => Ok(Value::Tensor(t.map(|x| negate(x).unwrap_or(Value::Error("negate failed".into()))))), + Value::Tensor(t) => Ok(Value::Tensor(Rc::new(t.map(|x| negate(x).unwrap_or(Value::Error("negate failed".into())))))), _ => Err(EvalError::type_error("numeric", &value)), } } @@ -823,15 +824,15 @@ fn scan(value: Value) -> EvalResult { Value::Tensor(t) => { let mut running = Value::Int(0); let scanned: Vec = t.iter().map(|v| { running = add(running.clone(), v).unwrap_or(Value::Error("scan failed".into())); running.clone() }).collect(); - Ok(Value::Tensor(Tensor::from_values(t.shape.clone(), scanned))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(t.shape.clone(), scanned)))) } _ => Err(EvalError::type_error("Tensor", &value)), } } fn len(value: Value) -> EvalResult { match value { Value::Tensor(t) => Ok(Value::Int(t.len() as i128)), Value::Tuple(vs) => Ok(Value::Int(vs.len() as i128)), _ => Err(EvalError::type_error("Tensor or Tuple", &value)) } } -fn shape(value: Value) -> EvalResult { match value { Value::Tensor(t) => Ok(Value::Tensor(Tensor::from_ints(t.shape.iter().map(|&d| d as i128).collect()))), _ => Err(EvalError::type_error("Tensor", &value)) } } -fn reverse(value: Value) -> EvalResult { match value { Value::Tensor(t) => { let mut data = t.to_vec(); data.reverse(); Ok(Value::Tensor(Tensor::from_values(t.shape.clone(), data))) } Value::Tuple(mut vs) => { vs.reverse(); Ok(Value::Tuple(vs)) } _ => Err(EvalError::type_error("Tensor or Tuple", &value)) } } +fn shape(value: Value) -> EvalResult { match value { Value::Tensor(t) => Ok(Value::Tensor(Rc::new(Tensor::from_ints(t.shape.iter().map(|&d| d as i128).collect())))), _ => Err(EvalError::type_error("Tensor", &value)) } } +fn reverse(value: Value) -> EvalResult { match value { Value::Tensor(t) => { let mut data = t.to_vec(); data.reverse(); Ok(Value::Tensor(Rc::new(Tensor::from_values(t.shape.clone(), data)))) } Value::Tuple(mut vs) => { vs.reverse(); Ok(Value::Tuple(vs)) } _ => Err(EvalError::type_error("Tensor or Tuple", &value)) } } fn concat(left: Value, right: Value) -> EvalResult { use crate::value::TensorData; @@ -855,7 +856,7 @@ fn concat(left: Value, right: Value) -> EvalResult { } _ => { let mut data = a.to_vec(); data.extend(b.iter()); TensorData::Generic(data) } }; - Ok(Value::Tensor(Tensor { shape: vec![new_len], data })) + Ok(Value::Tensor(Rc::new(Tensor { shape: vec![new_len], data }))) } (Value::Tuple(a), Value::Tuple(b)) => { let mut result = a.clone(); result.extend(b.iter().cloned()); Ok(Value::Tuple(result)) } _ => Err(EvalError::type_error_msg(format!("Cannot concat {} and {}", left.type_name(), right.type_name()))), @@ -867,7 +868,7 @@ fn zip_with(left: Value, right: Value) -> EvalResult { (Value::Tensor(a), Value::Tensor(b)) => { if a.shape != b.shape { return Err(EvalError::shape_mismatch(format!("Cannot zip tensors with shapes {:?} and {:?}", a.shape, b.shape))); } let zipped: Vec = a.iter().zip(b.iter()).map(|(x, y)| Value::Tuple(vec![x, y])).collect(); - Ok(Value::Tensor(Tensor::from_values(a.shape.clone(), zipped))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(a.shape.clone(), zipped)))) } _ => Err(EvalError::type_error_msg(format!("Cannot zip {} and {}", left.type_name(), right.type_name()))), } @@ -899,7 +900,7 @@ fn matmul(left: Value, right: Value) -> EvalResult { if n != b.shape[0] { return Err(EvalError::shape_mismatch(format!("Matrix dimensions incompatible: [{} {}] × [{} {}]", m, n, b.shape[0], p))); } let mut result = vec![Value::Float(OrderedFloat(0.0)); m * p]; for i in 0..m { for j in 0..p { let mut sum = 0.0f64; for k in 0..n { let a_ik = a.get(&[i, k]).and_then(|v| v.coerce_float()).unwrap_or(0.0); let b_kj = b.get(&[k, j]).and_then(|v| v.coerce_float()).unwrap_or(0.0); sum += a_ik * b_kj; } result[i * p + j] = Value::Float(OrderedFloat(sum)); } } - Ok(Value::Tensor(Tensor::from_values(vec![m, p], result))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![m, p], result)))) } _ => Err(EvalError::type_error("Tensor", &left)), } @@ -912,7 +913,7 @@ fn transpose(value: Value) -> EvalResult { let m = t.shape[0]; let n = t.shape[1]; let mut result = vec![Value::Float(OrderedFloat(0.0)); m * n]; for i in 0..m { for j in 0..n { if let Some(v) = t.get(&[i, j]) { result[j * m + i] = v; } } } - Ok(Value::Tensor(Tensor::from_values(vec![n, m], result))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![n, m], result)))) } _ => Err(EvalError::type_error("Tensor", &value)), } @@ -993,7 +994,7 @@ fn iota(n: Value) -> EvalResult { return Err(EvalError::type_error_msg("iota requires non-negative integer")); } let data: Vec = (0..count).collect(); - Ok(Value::Tensor(Tensor::from_ints(data))) + Ok(Value::Tensor(Rc::new(Tensor::from_ints(data)))) } _ => Err(EvalError::type_error("integer", &n)), } @@ -1004,7 +1005,7 @@ fn range(start: Value, end: Value) -> EvalResult { match (&start, &end) { (Value::Int(s), Value::Int(e)) => { let data: Vec = (*s..*e).collect(); - Ok(Value::Tensor(Tensor::from_ints(data))) + Ok(Value::Tensor(Rc::new(Tensor::from_ints(data)))) } _ => Err(EvalError::type_error_msg(format!( "range requires two integers, got {} and {}", @@ -1098,7 +1099,7 @@ fn take(n: Value, arr: Value) -> EvalResult { return Ok(Value::string(&taken)); } let data: Vec = (0..count).map(|i| t.get_flat(i).unwrap()).collect(); - Ok(Value::Tensor(Tensor::from_values(vec![data.len()], data))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![data.len()], data)))) } (Value::Int(count), Value::Tuple(vs)) => { let count = (*count).max(0) as usize; @@ -1128,7 +1129,7 @@ fn drop_fn(n: Value, arr: Value) -> EvalResult { return Ok(Value::string(&dropped)); } let data: Vec = (count..t.len()).map(|i| t.get_flat(i).unwrap()).collect(); - Ok(Value::Tensor(Tensor::from_values(vec![data.len()], data))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![data.len()], data)))) } (Value::Int(count), Value::Tuple(vs)) => { let count = (*count).max(0) as usize; @@ -1168,7 +1169,7 @@ fn lines(value: Value) -> EvalResult { Value::Tensor(t) => { if let Some(s) = t.to_string_value() { let line_strs: Vec = s.lines().map(|l| Value::string(l)).collect(); - Ok(Value::Tensor(Tensor::from_values(vec![line_strs.len()], line_strs))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![line_strs.len()], line_strs)))) } else { Err(EvalError::type_error("String", &Value::Tensor(t))) } @@ -1183,7 +1184,7 @@ fn words(value: Value) -> EvalResult { Value::Tensor(t) => { if let Some(s) = t.to_string_value() { let word_strs: Vec = s.split_whitespace().map(|w| Value::string(w)).collect(); - Ok(Value::Tensor(Tensor::from_values(vec![word_strs.len()], word_strs))) + Ok(Value::Tensor(Rc::new(Tensor::from_values(vec![word_strs.len()], word_strs)))) } else { Err(EvalError::type_error("String", &Value::Tensor(t))) } @@ -1198,7 +1199,7 @@ fn bytes(value: Value) -> EvalResult { Value::Tensor(t) => { if let Some(s) = t.to_string_value() { let byte_vals: Vec = s.as_bytes().iter().map(|&b| b as i128).collect(); - Ok(Value::Tensor(Tensor::from_ints(byte_vals))) + Ok(Value::Tensor(Rc::new(Tensor::from_ints(byte_vals)))) } else { Err(EvalError::type_error("String", &Value::Tensor(t))) } diff --git a/crates/goth-eval/src/value.rs b/crates/goth-eval/src/value.rs index 168a030..f61ebaa 100644 --- a/crates/goth-eval/src/value.rs +++ b/crates/goth-eval/src/value.rs @@ -13,11 +13,11 @@ pub enum Value { Bool(bool), Char(char), Unit, - Tensor(Tensor), + Tensor(Rc), Tuple(Vec), Record(Rc>), Variant { tag: String, payload: Option> }, - Closure(Closure), + Closure(Rc), Primitive(PrimFn), Partial { func: Box, args: Vec, remaining: usize }, Thunk(Thunk), @@ -97,7 +97,7 @@ impl Value { pub fn bool(b: bool) -> Self { Value::Bool(b) } pub fn char(c: char) -> Self { Value::Char(c) } pub fn unit() -> Self { Value::Unit } - pub fn string(s: &str) -> Self { Value::Tensor(Tensor::from_string(s)) } + pub fn string(s: &str) -> Self { Value::Tensor(Rc::new(Tensor::from_string(s))) } pub fn tuple(values: Vec) -> Self { if values.is_empty() { Value::Unit } else { Value::Tuple(values) } } @@ -106,10 +106,10 @@ impl Value { } pub fn error(msg: impl Into) -> Self { Value::Error(msg.into()) } pub fn closure(arity: u32, body: goth_ast::expr::Expr, env: Env) -> Self { - Value::Closure(Closure { arity, body, env, preconditions: vec![], postconditions: vec![] }) + Value::Closure(Rc::new(Closure { arity, body, env, preconditions: vec![], postconditions: vec![] })) } pub fn closure_with_contracts(arity: u32, body: goth_ast::expr::Expr, env: Env, preconditions: Vec, postconditions: Vec) -> Self { - Value::Closure(Closure { arity, body, env, preconditions, postconditions }) + Value::Closure(Rc::new(Closure { arity, body, env, preconditions, postconditions })) } pub fn primitive(prim: PrimFn) -> Self { Value::Primitive(prim) } diff --git a/examples/contracts/closure_reuse_contract.goth b/examples/contracts/closure_reuse_contract.goth new file mode 100644 index 0000000..2ff6bfb --- /dev/null +++ b/examples/contracts/closure_reuse_contract.goth @@ -0,0 +1,13 @@ +# Closure remains valid after partial application +# Input: n +# Tests that creating partial applications doesn't corrupt the original closure + +╭─ main : I64 → Bool +│ ⊨ ₀ = true +╰─ let n = ₀ in + let add = λ→ λ→ ₁ + ₀ in + let g = add 5 in + let h = add 10 in + let a = g n in + let b = h n in + a + b = n + n + 15 diff --git a/examples/contracts/tensor_identity_contract.goth b/examples/contracts/tensor_identity_contract.goth new file mode 100644 index 0000000..3276741 --- /dev/null +++ b/examples/contracts/tensor_identity_contract.goth @@ -0,0 +1,11 @@ +# Identity map preserves value equality +# Input: n (size of array to test) +# Postcondition: identity-mapping an array produces equal result +# Tests that tensor operations don't corrupt data through sharing + +╭─ main : I64 → Bool +│ ⊢ ₀ > 0 +│ ⊨ ₀ = true +╰─ let original = ι ₀ in + let mapped = original ↦ (λ→ ₀) in + original ≡ mapped diff --git a/examples/contracts/tensor_independence_contract.goth b/examples/contracts/tensor_independence_contract.goth new file mode 100644 index 0000000..4edbd8e --- /dev/null +++ b/examples/contracts/tensor_independence_contract.goth @@ -0,0 +1,10 @@ +# Mapping a tensor does not affect the original +# Input: n (size of array) +# Tests that tensor sharing doesn't cause mutation leaks + +╭─ main : I64 → Bool +│ ⊢ ₀ > 0 +│ ⊨ ₀ = true +╰─ let xs = ι ₀ in + let ys = xs ↦ (λ→ ₀ × 2) in + xs[0] = 0 ∧ ys[0] = 0