on master help sign in
Advance a contract by an index; remember the top a navigation climbed to
ddbff9fiko
src/repo.rs+32
@@ -82,8 +82,40 @@ fn id_of(commit: &Value) -> Result<String, Crash> {
82 }
82 }
83}
83}
84
84
85+thread_local! {
86+ /// The last location climbed to the top, and the value it was climbed
87+ /// from. A revset walk applies `by` to the same repo once per id, and
88+ /// each climb rebuilds a location per level of depth; the answer only
89+ /// depends on the value, which cannot change. Holding the `Rc` keeps the
90+ /// address from being reused while it is the key.
91+ static TOP_MEMO: std::cell::RefCell<Option<(std::rc::Rc<crate::value::RecordMap>, Value)>> =
92+ const { std::cell::RefCell::new(None) };
93+}
94+
85/// move a location to the top of history
95/// move a location to the top of history
86fn top_of(repo: &Value) -> Result<Value, Crash> {
96fn top_of(repo: &Value) -> Result<Value, Crash> {
97+ let key = match repo {
98+ Value::Record(m) => Some(m.clone()),
99+ _ => None,
100+ };
101+ if let Some(k) = &key {
102+ let hit = TOP_MEMO.with(|c| {
103+ c.borrow().as_ref().and_then(|(cached, top)| {
104+ std::rc::Rc::ptr_eq(cached, k).then(|| top.clone())
105+ })
106+ });
107+ if let Some(top) = hit {
108+ return Ok(top);
109+ }
110+ }
111+ let top = top_of_uncached(repo)?;
112+ if let Some(k) = key {
113+ TOP_MEMO.with(|c| *c.borrow_mut() = Some((k, top.clone())));
114+ }
115+ Ok(top)
116+}
117+
118+fn top_of_uncached(repo: &Value) -> Result<Value, Crash> {
87 let mut cur = repo.clone();
119 let mut cur = repo.clone();
88 loop {
120 loop {
89 // ask whether the context is empty without copying it: this runs once
121 // ask whether the context is empty without copying it: this runs once
src/shape.rs+30−20
@@ -134,10 +134,12 @@ fn is_type_var(n: &str) -> bool {
134}
134}
135
135
136/// A compiled signature: a chain of parameter types and a final result type.
136/// A compiled signature: a chain of parameter types and a final result type.
137+/// The types are shared, so handing one out is a refcount bump rather than a
138+/// deep copy — contracts are consulted on every call.
137#[derive(Clone, Debug)]
139#[derive(Clone, Debug)]
138pub struct Contract {
140pub struct Contract {
139− pub params: Vec<TypeExpr>,
141+ pub params: Vec<Rc2<TypeExpr>>,
140− pub result: TypeExpr,
142+ pub result: Rc2<TypeExpr>,
141}
143}
142
144
143pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
145pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
@@ -146,7 +148,7 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
146 loop {
148 loop {
147 match cur {
149 match cur {
148 TypeExpr::Fun(a, b) => {
150 TypeExpr::Fun(a, b) => {
149− params.push((**a).clone());
151+ params.push(a.clone());
150 cur = b;
152 cur = b;
151 }
153 }
152 TypeExpr::Con(n) => {
154 TypeExpr::Con(n) => {
@@ -164,7 +166,7 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
164 }
166 }
165 Contract {
167 Contract {
166 params,
168 params,
167− result: cur.clone(),
169+ result: Rc2::new(cur.clone()),
168 }
170 }
169}
171}
170
172
@@ -275,7 +277,10 @@ use std::rc::Rc as Rc2;
275pub enum ContractExpr {
277pub enum ContractExpr {
276 /// no contract information; arg checks pass through
278 /// no contract information; arg checks pass through
277 Unknown,
279 Unknown,
280+ /// a signature with its first `at` parameters already supplied; advancing
281+ /// it is an index bump, where rebuilding the shortened parameter list
282+ /// copied every remaining type on every application
278− Known(Rc2<Contract>),
283+ Known { contract: Rc2<Contract>, at: usize },
279 /// compose(f, g): (f . g); both contract expressions
284 /// compose(f, g): (f . g); both contract expressions
280 Compose(Box<ContractExpr>, Box<ContractExpr>),
285 Compose(Box<ContractExpr>, Box<ContractExpr>),
281 /// apply e to an argument that checked against e's first parameter
286 /// apply e to an argument that checked against e's first parameter
@@ -284,10 +289,10 @@ pub enum ContractExpr {
284
289
285impl ContractExpr {
290impl ContractExpr {
286 /// The type of the next parameter this function expects, if known.
291 /// The type of the next parameter this function expects, if known.
287− pub fn next_param(&self) -> Option<TypeExpr> {
292+ pub fn next_param(&self) -> Option<Rc2<TypeExpr>> {
288 match self {
293 match self {
289 ContractExpr::Unknown => None,
294 ContractExpr::Unknown => None,
290− ContractExpr::Known(c) => c.params.first().cloned(),
295+ ContractExpr::Known { contract, at } => contract.params.get(*at).cloned(),
291 ContractExpr::Compose(f, g) => g
296 ContractExpr::Compose(f, g) => g
292 .next_param()
297 .next_param()
293 .or_else(|| f.next_param())
298 .or_else(|| f.next_param())
@@ -301,12 +306,12 @@ impl ContractExpr {
301 }
306 }
302
307
303 /// The result type once all parameters are supplied.
308 /// The result type once all parameters are supplied.
304− pub fn result_after(&self) -> Option<TypeExpr> {
309+ pub fn result_after(&self) -> Option<Rc2<TypeExpr>> {
305 match self {
310 match self {
306 ContractExpr::Unknown => None,
311 ContractExpr::Unknown => None,
307− ContractExpr::Known(c) => {
312+ ContractExpr::Known { contract, at } => {
308− if c.params.is_empty() {
313+ if *at >= contract.params.len() {
309− Some(c.result.clone())
314+ Some(contract.result.clone())
310 } else {
315 } else {
311 None
316 None
312 }
317 }
@@ -326,7 +331,7 @@ impl ContractExpr {
326 pub fn is_exhausted(&self) -> bool {
331 pub fn is_exhausted(&self) -> bool {
327 match self {
332 match self {
328 ContractExpr::Unknown => false,
333 ContractExpr::Unknown => false,
329− ContractExpr::Known(c) => c.params.is_empty(),
334+ ContractExpr::Known { contract, at } => *at >= contract.params.len(),
330 ContractExpr::Compose(_, g) => g.is_exhausted(),
335 ContractExpr::Compose(_, g) => g.is_exhausted(),
331 ContractExpr::ApplyFirst(_) => false,
336 ContractExpr::ApplyFirst(_) => false,
332 }
337 }
@@ -334,19 +339,24 @@ impl ContractExpr {
334
339
335 /// Advance after one argument has been supplied and checked.
340 /// Advance after one argument has been supplied and checked.
336 pub fn apply_first_rc(self: Rc2<ContractExpr>) -> ContractExpr {
341 pub fn apply_first_rc(self: Rc2<ContractExpr>) -> ContractExpr {
342+ // the common case advances without allocating anything
343+ match &*self {
344+ ContractExpr::Unknown => ContractExpr::Unknown,
345+ ContractExpr::Known { contract, at } => ContractExpr::Known {
346+ contract: contract.clone(),
347+ at: contract.params.len().min(at + 1),
348+ },
337− ContractExpr::apply_first(Box::new((*self).clone()))
349+ _ => ContractExpr::apply_first(Box::new((*self).clone())),
350+ }
338 }
351 }
339
352
340 /// Advance after one argument has been supplied and checked.
353 /// Advance after one argument has been supplied and checked.
341 pub fn apply_first(self: Box<ContractExpr>) -> ContractExpr {
354 pub fn apply_first(self: Box<ContractExpr>) -> ContractExpr {
342 match *self {
355 match *self {
343 ContractExpr::Unknown => ContractExpr::Unknown,
356 ContractExpr::Unknown => ContractExpr::Unknown,
344− ContractExpr::Known(c) => {
357+ ContractExpr::Known { contract, at } => {
345− let mut c2 = (*c).clone();
358+ let at = contract.params.len().min(at + 1);
346− if !c2.params.is_empty() {
347− c2.params.remove(0);
348− }
349− ContractExpr::Known(Rc2::new(c2))
359+ ContractExpr::Known { contract, at }
350 }
360 }
351 ContractExpr::Compose(f, g) => {
361 ContractExpr::Compose(f, g) => {
352 if !g.is_exhausted() {
362 if !g.is_exhausted() {
@@ -363,7 +373,7 @@ impl ContractExpr {
363 /// Number of remaining parameters, if known and not nested.
373 /// Number of remaining parameters, if known and not nested.
364 pub fn params_len(&self) -> usize {
374 pub fn params_len(&self) -> usize {
365 match self {
375 match self {
366− ContractExpr::Known(c) => c.params.len(),
376+ ContractExpr::Known { contract, at } => contract.params.len() - at,
367 _ => 99,
377 _ => 99,
368 }
378 }
369 }
379 }
src/value.rs+1−1
@@ -459,7 +459,7 @@ impl Value {
459pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract>) -> Value {
459pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract>) -> Value {
460 let pending = Some((
460 let pending = Some((
461 name.to_string(),
461 name.to_string(),
462− Rc::new(crate::shape::ContractExpr::Known(contract)),
462+ Rc::new(crate::shape::ContractExpr::Known { contract, at: 0 }),
463 ));
463 ));
464 match v {
464 match v {
465 Value::Fun(fv) => match fv.as_ref() {
465 Value::Fun(fv) => match fv.as_ref() {
tests/laws.rs+45
@@ -642,3 +642,48 @@ fn by_then_climbing_back_restores_the_history() {
642 );
642 );
643 }
643 }
644}
644}
645+
646+#[test]
647+fn repeated_navigation_is_consistent() {
648+ // `top_of` is memoised on the identity of the value it was asked about,
649+ // and `by` is applied to the same repo once per id in a revset walk. The
650+ // memo must not let one navigation affect the next, including across
651+ // repos that differ only in where they are focused.
652+ let (repo, ids) = linear_repo(6);
653+ let mut seen: Vec<Value> = Vec::new();
654+ for id in &ids {
655+ seen.push(j::repo::by_id(&repo, id).unwrap().expect("found"));
656+ }
657+ // asking again, interleaved with other repos, gives the same answers
658+ let (other, other_ids) = linear_repo(4);
659+ for (k, id) in ids.iter().enumerate() {
660+ let again = j::repo::by_id(&repo, id).unwrap().expect("found");
661+ assert!(
662+ value_eq(&again, &seen[k]).unwrap_or(false),
663+ "second lookup of {} differs",
664+ id
665+ );
666+ // navigating a different repo in between must not disturb it
667+ let o = j::repo::by_id(&other, &other_ids[0]).unwrap().expect("found");
668+ match o.field("root").unwrap().field("id").unwrap() {
669+ Value::Id(f) => assert_eq!(&*f, &other_ids[0]),
670+ _ => panic!("no id"),
671+ }
672+ let third = j::repo::by_id(&repo, id).unwrap().expect("found");
673+ assert!(
674+ value_eq(&third, &seen[k]).unwrap_or(false),
675+ "lookup of {} disturbed by another repo",
676+ id
677+ );
678+ }
679+ // and navigating from an already-refocused location still reaches the top
680+ for loc in &seen {
681+ for id in &ids {
682+ let from_loc = j::repo::by_id(loc, id).unwrap().expect("found");
683+ match from_loc.field("root").unwrap().field("id").unwrap() {
684+ Value::Id(f) => assert_eq!(&*f, id, "reached the wrong commit"),
685+ _ => panic!("no id"),
686+ }
687+ }
688+ }
689+}