on master help sign in
Advance a contract by an index; remember the top a navigation climbed to

Two costs every program in the language paid, found by timing a 100k-element loop against the same loop with one more builtin call in it: a lambda application took 0.45us, and a call to a builtin with a signature took 2.6us on top of it. Almost all of that was the contract machinery rebuilding itself: - Advancing past a supplied argument cloned the whole `Contract` and removed the first parameter, so every call copied every remaining parameter type. `ContractExpr::Known` now carries how far it has been applied, and advancing is an index bump. The parameter and result types are shared rather than owned, so handing one to a check is a refcount bump. - `check` unfolded the declared type by cloning it — a `Vec` of `String`s for a record type — and then asked `shape_of` for a record shape that was rebuilt, field set and all, on every call. `unfold` borrows now and `Shapes` memoises; declarations do not change once the config is loaded. One builtin call per element over 100k elements: 302ms -> 161ms. Separately, `by` climbed to the top of the history on every call, rebuilding a location per level. A revset walk applies it once per id, so the climbing was quadratic: `commitAt` over every commit of a 401-commit history spent 373ms of its 509ms re-climbing. `top_of` now remembers the last value it was asked about, keyed on that value's identity — it is pure, and the `Rc` is held so the address cannot be reused. Same walk: 509ms -> 154ms. On the 401-commit repository: conflicts 1576ms -> 953ms, treeData 734 -> 663, tree 203 -> 196, log 162 -> 124, status 110 -> 106. On an 800-commit chain, conflicts 2354ms -> 1052ms. 310 command outputs across ten repositories are unchanged. The memo is the part that could go quietly wrong, so a test navigates the same repo repeatedly, interleaved with a different one and with already-refocused locations. Making the memo ignore its key fails eight tests.

commitddbff9f2f45108b37908de987218fc95580c7d76 authoriko <ilyakooo0@gmail.com> dateTue 22 Sep 2026, 17:36 (3 weeks ago) parent6f838ec
4 files changed+108−21
src/repo.rs+32 src/shape.rs+30−20 src/value.rs+1−1 tests/laws.rs+45
src/repo.rs+32
@@ -82,8 +82,40 @@ fn id_of(commit: &Value) -> Result<String, Crash> {
8282 }
8383}
8484
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+
8595/// move a location to the top of history
8696fn 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> {
87119 let mut cur = repo.clone();
88120 loop {
89121 // 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 {
134134}
135135
136136/// 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.
137139#[derive(Clone, Debug)]
138140pub struct Contract {
139− pub params: Vec<TypeExpr>,
140− pub result: TypeExpr,
141+ pub params: Vec<Rc2<TypeExpr>>,
142+ pub result: Rc2<TypeExpr>,
141143}
142144
143145pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
@@ -146,7 +148,7 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
146148 loop {
147149 match cur {
148150 TypeExpr::Fun(a, b) => {
149− params.push((**a).clone());
151+ params.push(a.clone());
150152 cur = b;
151153 }
152154 TypeExpr::Con(n) => {
@@ -164,7 +166,7 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
164166 }
165167 Contract {
166168 params,
167− result: cur.clone(),
169+ result: Rc2::new(cur.clone()),
168170 }
169171}
170172
@@ -275,7 +277,10 @@ use std::rc::Rc as Rc2;
275277pub enum ContractExpr {
276278 /// no contract information; arg checks pass through
277279 Unknown,
278− Known(Rc2<Contract>),
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
283+ Known { contract: Rc2<Contract>, at: usize },
279284 /// compose(f, g): (f . g); both contract expressions
280285 Compose(Box<ContractExpr>, Box<ContractExpr>),
281286 /// apply e to an argument that checked against e's first parameter
@@ -284,10 +289,10 @@ pub enum ContractExpr {
284289
285290impl ContractExpr {
286291 /// 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>> {
288293 match self {
289294 ContractExpr::Unknown => None,
290− ContractExpr::Known(c) => c.params.first().cloned(),
295+ ContractExpr::Known { contract, at } => contract.params.get(*at).cloned(),
291296 ContractExpr::Compose(f, g) => g
292297 .next_param()
293298 .or_else(|| f.next_param())
@@ -301,12 +306,12 @@ impl ContractExpr {
301306 }
302307
303308 /// 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>> {
305310 match self {
306311 ContractExpr::Unknown => None,
307− ContractExpr::Known(c) => {
308− if c.params.is_empty() {
309− Some(c.result.clone())
312+ ContractExpr::Known { contract, at } => {
313+ if *at >= contract.params.len() {
314+ Some(contract.result.clone())
310315 } else {
311316 None
312317 }
@@ -326,7 +331,7 @@ impl ContractExpr {
326331 pub fn is_exhausted(&self) -> bool {
327332 match self {
328333 ContractExpr::Unknown => false,
329− ContractExpr::Known(c) => c.params.is_empty(),
334+ ContractExpr::Known { contract, at } => *at >= contract.params.len(),
330335 ContractExpr::Compose(_, g) => g.is_exhausted(),
331336 ContractExpr::ApplyFirst(_) => false,
332337 }
@@ -334,19 +339,24 @@ impl ContractExpr {
334339
335340 /// Advance after one argument has been supplied and checked.
336341 pub fn apply_first_rc(self: Rc2<ContractExpr>) -> ContractExpr {
337− ContractExpr::apply_first(Box::new((*self).clone()))
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+ },
349+ _ => ContractExpr::apply_first(Box::new((*self).clone())),
350+ }
338351 }
339352
340353 /// Advance after one argument has been supplied and checked.
341354 pub fn apply_first(self: Box<ContractExpr>) -> ContractExpr {
342355 match *self {
343356 ContractExpr::Unknown => ContractExpr::Unknown,
344− ContractExpr::Known(c) => {
345− let mut c2 = (*c).clone();
346− if !c2.params.is_empty() {
347− c2.params.remove(0);
348− }
349− ContractExpr::Known(Rc2::new(c2))
357+ ContractExpr::Known { contract, at } => {
358+ let at = contract.params.len().min(at + 1);
359+ ContractExpr::Known { contract, at }
350360 }
351361 ContractExpr::Compose(f, g) => {
352362 if !g.is_exhausted() {
@@ -363,7 +373,7 @@ impl ContractExpr {
363373 /// Number of remaining parameters, if known and not nested.
364374 pub fn params_len(&self) -> usize {
365375 match self {
366− ContractExpr::Known(c) => c.params.len(),
376+ ContractExpr::Known { contract, at } => contract.params.len() - at,
367377 _ => 99,
368378 }
369379 }
src/value.rs+1−1
@@ -459,7 +459,7 @@ impl Value {
459459pub fn attach_pending(v: &Value, name: &str, contract: Rc<crate::shape::Contract>) -> Value {
460460 let pending = Some((
461461 name.to_string(),
462− Rc::new(crate::shape::ContractExpr::Known(contract)),
462+ Rc::new(crate::shape::ContractExpr::Known { contract, at: 0 }),
463463 ));
464464 match v {
465465 Value::Fun(fv) => match fv.as_ref() {
tests/laws.rs+45
@@ -642,3 +642,48 @@ fn by_then_climbing_back_restores_the_history() {
642642 );
643643 }
644644}
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+}