diff --git a/src/eval/cache/incremental.rs b/src/eval/cache/incremental.rs index 5ceccf545..1833b1536 100644 --- a/src/eval/cache/incremental.rs +++ b/src/eval/cache/incremental.rs @@ -70,7 +70,7 @@ impl IncCache { idx } - fn revnode_as_explicit_fun<'a, I>(node: &IncNode, args: I) -> IncNode + fn revnode_as_explicit_fun<'a, I>(node: &mut IncNode, args: I) where I: DoubleEndedIterator, { @@ -85,16 +85,12 @@ impl IncCache { let as_function = args.rfold(body, |built, id| RichTerm::from(Term::Fun(*id, built))); - IncNode::new( - Closure { - body: as_function, - env, - }, - node.kind, - node.bty.clone(), - ) + node.orig = Closure { + body: as_function, + env, + } } - _ => node.clone(), + _ => (), } } @@ -131,6 +127,27 @@ impl IncCache { } } + fn propagate_dirty_vec(&mut self, indices: Vec) { + let mut visited = HashSet::new(); + let mut stack = indices; + + while !stack.is_empty() { + let i = stack.pop().unwrap(); + visited.insert(i); + let mut current_node = self.store.get_mut(i).unwrap(); + current_node.cached = None; + current_node.state = IncNodeState::Suspended; + println!("IDX: {:?} BLs: {:?}", i, current_node.backlinks); + stack.extend( + current_node + .backlinks + .iter() + .map(|x| x.idx) + .filter(|x| !visited.contains(&x)), + ) + } + } + /* Do we need this when we can revert in place? fn propagate_revert(&mut self, id: Ident, idx: CacheIndex) -> HashMap { @@ -174,11 +191,30 @@ impl IncCache { let current_node = self.store.get_mut(*i).unwrap(); for dep in current_node.backlinks.iter_mut() { - dep.idx = *new_indices.get(i).unwrap(); + dep.idx = if let Some(idx) = new_indices.get(&dep.idx) { + *idx + } else { + dep.idx + } } + let mut to_be_updated = vec![]; + for dep in current_node.fwdlinks.iter_mut() { - dep.idx = *new_indices.get(i).unwrap(); + dep.idx = if let Some(idx) = new_indices.get(&dep.idx) { + *idx + } else { + to_be_updated.push(dep.clone()); + dep.idx + } + } + + for dep in to_be_updated { + let target_node = self.store.get_mut(dep.idx).unwrap(); + target_node.backlinks.push(DependencyLink { + id: dep.id, + idx: *i, + }); } } @@ -345,7 +381,7 @@ impl Cache for IncCache { env: &mut Environment, fields: I, ) -> RichTerm { - let node = self.store.get(idx).unwrap(); + let node = self.store.get_mut(idx).unwrap(); let mut deps_filter: Box bool> = match node.bty.clone() { BindingType::Revertible(FieldDeps::Known(deps)) => { @@ -355,13 +391,10 @@ impl Cache for IncCache { BindingType::Normal => Box::new(|_: &&Ident| false), }; - let node_as_function = self.add_node(IncCache::revnode_as_explicit_fun( - node, - fields.clone().filter(&mut deps_filter), - )); + IncCache::revnode_as_explicit_fun(node, fields.clone().filter(&mut deps_filter)); let fresh_var = Ident::fresh(); - env.insert(fresh_var, node_as_function); + env.insert(fresh_var, idx); let as_function_closurized = RichTerm::from(Term::Var(fresh_var)); let args = fields.filter_map(|id| deps_filter(&id).then(|| RichTerm::from(Term::Var(*id)))); @@ -370,4 +403,12 @@ impl Cache for IncCache { RichTerm::from(Term::App(partial_app, arg)) }) } + + fn smart_clone(&mut self, v: Vec) -> HashMap { + self.smart_clone(v) + } + + fn propagate_dirty(&mut self, indices: Vec) { + self.propagate_dirty_vec(indices); + } } diff --git a/src/eval/cache/lazy.rs b/src/eval/cache/lazy.rs index 552eabcba..e93a0ea14 100644 --- a/src/eval/cache/lazy.rs +++ b/src/eval/cache/lazy.rs @@ -4,8 +4,11 @@ use crate::{ identifier::Ident, term::{record::FieldDeps, BindingType, RichTerm, Term}, }; -use std::cell::{Ref, RefCell, RefMut}; use std::rc::{Rc, Weak}; +use std::{ + cell::{Ref, RefCell, RefMut}, + collections::HashMap, +}; /// The state of a thunk. /// @@ -355,7 +358,7 @@ impl ThunkData { /// inside a record may be invalidated by merging, and thus need to store the unaltered original /// expression. Those aspects are handled and discussed in more detail in /// [InnerThunkData]. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub struct Thunk { data: Rc>, ident_kind: IdentKind, @@ -560,6 +563,21 @@ impl Thunk { self.data.borrow().deps() } } + +impl PartialEq for Thunk { + fn eq(&self, other: &Self) -> bool { + self.data.as_ptr() == other.data.as_ptr() && self.ident_kind == other.ident_kind + } +} + +impl Eq for Thunk {} + +impl std::hash::Hash for Thunk { + fn hash(&self, state: &mut H) { + let raw_ptr = self.data.as_ptr(); + (self.ident_kind, raw_ptr).hash(state) + } +} /// A thunk update frame. /// /// A thunk update frame is put on the stack whenever a variable is entered, such that once this @@ -692,4 +710,15 @@ impl Cache for CBNCache { ) -> Result { idx.mk_update_frame() } + + fn smart_clone( + &mut self, + v: Vec, + ) -> std::collections::HashMap { + v.into_iter() + .map(|idx| (idx.clone(), self.revert(&idx))) + .collect() + } + + fn propagate_dirty(&mut self, indices: Vec) {} } diff --git a/src/eval/cache/mod.rs b/src/eval/cache/mod.rs index 221732b02..12c2c971d 100644 --- a/src/eval/cache/mod.rs +++ b/src/eval/cache/mod.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + /// The Nickel generic evaluation cache. This module abstracts away the details for managing /// suspended computations and their memoization strategies. /// @@ -94,4 +96,8 @@ pub trait Cache: Clone { &mut self, idx: &mut CacheIndex, ) -> Result; + + fn smart_clone(&mut self, v: Vec) -> HashMap; + + fn propagate_dirty(&mut self, indices: Vec); } diff --git a/src/eval/fixpoint.rs b/src/eval/fixpoint.rs index f664e9860..01d475c54 100644 --- a/src/eval/fixpoint.rs +++ b/src/eval/fixpoint.rs @@ -1,6 +1,8 @@ //! Compute the fixpoint of a recursive record. -use super::{merge::RevertClosurize, *}; -use crate::{label::Label, position::TermPos}; +use std::collections::HashSet; + +use super::{merge::{field_deps, RevertClosurize}, *}; +use crate::{position::TermPos, term::record::FieldDeps, label::Label}; // Update the environment of a term by extending it with a recursive environment. In the general // case, the term is expected to be a variable pointing to the element to be patched. Otherwise, it's @@ -83,7 +85,7 @@ pub fn rec_env<'a, I: Iterator, C: Cache>( // so we start from in the environment of the original record. let mut final_env = env.clone(); let id_value = Ident::fresh(); - final_env.insert(id_value, idx); + final_env.insert(id_value, idx.clone()); let with_ctr_applied = PendingContract::apply_all( RichTerm::new(Term::Var(id_value), value.pos), @@ -131,10 +133,15 @@ pub fn rec_env<'a, I: Iterator, C: Cache>( env: final_env, }; - Ok(( - *id, - cache.add(final_closure, IdentKind::Record, BindingType::Normal), - )) + let deps = FieldDeps::from(HashSet::from([*id])); + let mut new_idx = cache.add( + final_closure, + IdentKind::Record, + BindingType::Revertible(deps), + ); + cache.build_cached(&mut new_idx, &[(*id, idx)]); + + Ok((*id, new_idx)) } else { let error = EvalError::MissingFieldDef { id: *id, diff --git a/src/eval/merge.rs b/src/eval/merge.rs index 9820780fe..26eac427f 100644 --- a/src/eval/merge.rs +++ b/src/eval/merge.rs @@ -210,11 +210,28 @@ pub fn merge( }); } + let mut env = Environment::new(); + let fields_1 = smart_clone_closurize(cache, r1.fields, &mut env, &env1); + let fields_2 = smart_clone_closurize(cache, r2.fields, &mut env, &env2); + let hashmap::SplitResult { left, center, right, - } = hashmap::split(r1.fields, r2.fields); + } = hashmap::split(fields_1, fields_2); + + let mut center_indices = Vec::with_capacity(2 * center.len()); + + for (field_l, field_r) in center.values() { + if let Some(idx) = field_l.get_idx(&env) { + center_indices.push(idx); + } + if let Some(idx) = field_r.get_idx(&env) { + center_indices.push(idx); + } + } + + cache.propagate_dirty(center_indices); match mode { MergeMode::Contract(label) if !r2.attrs.open && !left.is_empty() => { @@ -255,7 +272,6 @@ Append `, ..` at the end of the record contract, as in `{some_field | SomeContra .cloned() .collect(); let mut m = HashMap::with_capacity(left.len() + center.len() + right.len()); - let mut env = Environment::new(); // Merging recursive records is the one operation that may override recursive fields. To // have the recursive fields depend on the updated values, we need to revert the @@ -266,16 +282,9 @@ Append `, ..` at the end of the record contract, as in `{some_field | SomeContra // The fields in the intersection (center) need a slightly more general treatment to // correctly propagate the recursive values down each field: saturation. See // [crate::eval::cache::Cache::saturate()]. - m.extend( - left.into_iter() - .map(|(id, field)| (id, field.revert_closurize(cache, &mut env, env1.clone()))), - ); - m.extend( - right - .into_iter() - .map(|(id, field)| (id, field.revert_closurize(cache, &mut env, env2.clone()))), - ); + m.extend(left.into_iter()); + m.extend(right.into_iter()); for (id, (field1, field2)) in center.into_iter() { m.insert( @@ -284,9 +293,9 @@ Append `, ..` at the end of the record contract, as in `{some_field | SomeContra cache, merge_label, field1, - env1.clone(), + env.clone(), field2, - env2.clone(), + env.clone(), &mut env, field_names.iter(), )?, @@ -346,7 +355,7 @@ fn merge_fields<'a, C: Cache, I: DoubleEndedIterator + Clone>( let Field { metadata: metadata1, value: value1, - pending_contracts: pending_contracts1, + pending_contracts: pending_contracts1, //TODO smart_clone should take care of these } = field1; let Field { metadata: metadata2, @@ -364,26 +373,17 @@ fn merge_fields<'a, C: Cache, I: DoubleEndedIterator + Clone>( ), metadata1.priority, ), - (Some(t1), _) if metadata1.priority > metadata2.priority => ( - Some(t1.revert_closurize(cache, env_final, env1.clone())), - metadata1.priority, - ), - (Some(t1), None) => ( - Some(t1.revert_closurize(cache, env_final, env1.clone())), - metadata1.priority, - ), - (_, Some(t2)) if metadata2.priority > metadata1.priority => ( - Some(t2.revert_closurize(cache, env_final, env2.clone())), - metadata2.priority, - ), - (None, Some(t2)) => ( - Some(t2.revert_closurize(cache, env_final, env2.clone())), - metadata2.priority, - ), + (Some(t1), _) if metadata1.priority > metadata2.priority => (Some(t1), metadata1.priority), + (Some(t1), None) => (Some(t1), metadata1.priority), + (_, Some(t2)) if metadata2.priority > metadata1.priority => (Some(t2), metadata2.priority), + (None, Some(t2)) => (Some(t2), metadata2.priority), (None, None) => (None, Default::default()), _ => unreachable!(), }; + //let mut pending_contracts = pending_contracts1; + //pending_contracts.extend(pending_contracts2.into_iter()); + let mut pending_contracts = pending_contracts1.revert_closurize(cache, env_final, env1.clone()); pending_contracts.extend( pending_contracts2 @@ -481,7 +481,7 @@ impl Saturate for RichTerm { } /// Return the dependencies of a field when represented as a `RichTerm`. -fn field_deps( +pub fn field_deps( cache: &C, rt: &RichTerm, local_env: &Environment, @@ -545,6 +545,83 @@ fn fields_merge_closurize<'a, I: DoubleEndedIterator + Clone, Ok(RichTerm::from(Term::Var(fresh_var))) } +fn smart_clone_closurize( + cache: &mut C, + mut fields: HashMap, + env: &mut Environment, + with_env: &Environment, +) -> HashMap { + let index_map: HashMap<_, _> = fields + .iter() + .map(|(id, field)| { + let contracts: Vec<_> = field + .pending_contracts + .iter() + .map(|contract| contract.contract.get_idx(with_env)) + .collect(); + (*id, (field.get_idx(with_env), contracts)) + }) + .collect(); + let to_smart_clone = index_map + .values() + .flat_map(|(field_idx, contract_idxs)| { + std::iter::once(field_idx).chain(contract_idxs.iter()) + }) + .filter_map(|x| x.as_ref().cloned()) + .collect(); + let new_indices = cache.smart_clone(to_smart_clone); + + for (id, (old_idx, old_contract_indices)) in index_map { + if let Some(idx) = old_idx { + let new_idx = new_indices.get(&idx).unwrap(); + let fresh_ident = Ident::fresh(); + env.insert(fresh_ident, new_idx.clone()); + let mut field = fields.get_mut(&id).unwrap(); + let pos = field.value.as_ref().unwrap().pos; + field.value = Some(RichTerm::new(Term::Var(fresh_ident), pos)); + } + for (i, idx) in old_contract_indices + .into_iter() + .enumerate() + .filter_map(|(id, idx)| Some((id, idx?))) + { + let new_idx = new_indices.get(&idx).unwrap(); + let fresh_ident = Ident::fresh(); + env.insert(fresh_ident, new_idx.clone()); + let mut pending_contract = fields + .get_mut(&id) + .unwrap() + .pending_contracts + .get_mut(i) + .unwrap(); + let pos = pending_contract.contract.pos; + pending_contract.contract = RichTerm::new(Term::Var(fresh_ident), pos); + } + } + + fields +} + +trait GetCacheIndex { + fn get_idx(&self, env: &Environment) -> Option; +} + +impl GetCacheIndex for Field { + fn get_idx(&self, env: &Environment) -> Option { + self.value.as_ref().and_then(|v| v.get_idx(env)) + } +} + +impl GetCacheIndex for RichTerm { + fn get_idx(&self, env: &Environment) -> Option { + if let Term::Var(id) = self.as_ref() { + env.get(id).cloned() + } else { + None + } + } +} + /// Same as [Closurizable], but also revert the element if the term is a variable. pub(super) trait RevertClosurize { /// Revert the element at the index inside the term (if any), and closurize the result inside `env`. diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 2da43ff32..c42dfc9d0 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -805,7 +805,7 @@ impl VirtualMachine { } /// Kind of an identifier. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum IdentKind { Let, Lambda,