on master help sign in
Walk the zipper once per navigation, not once per level
6f838eciko
src/repo.rs+71−13
@@ -7,21 +7,72 @@ use crate::value::{value_eq, Crash, Value};
7use std::collections::{BTreeMap, BTreeSet};
7use std::collections::{BTreeMap, BTreeSet};
8
8
9/// navigate: refocus `repo` on the commit with change id `id` (§10 reference).
9/// navigate: refocus `repo` on the commit with change id `id` (§10 reference).
10+///
11+/// Finding the commit and refocusing onto it are separate walks: refocusing
12+/// builds a new location — a record, with the siblings split around a hole —
13+/// so doing it for every node visited on the way cost an allocation per
14+/// commit. Only the commits on the path to the target need one, and there are
15+/// as many of those as the history is deep.
10pub fn by_id(repo: &Value, id: &str) -> Result<Option<Value>, Crash> {
16pub fn by_id(repo: &Value, id: &str) -> Result<Option<Value>, Crash> {
11 let top = top_of(repo)?;
17 let top = top_of(repo)?;
12− let mut stack = vec![top];
18+ if id_is(&top.field("root")?, id)? {
13− while let Some(loc) = stack.pop() {
19+ return Ok(Some(top));
14− let root_id = id_of(&loc.field("root")?)?;
20+ }
15− if root_id == id {
21+ let mut path = Vec::new();
22+ let children = top.field("children")?;
23+ if !find_path(children.as_list()?, id, &mut path)? {
24+ return Ok(None);
25+ }
26+ // Descend the path collecting one frame per level and assemble the
27+ // location once. Refocusing step by step would rebuild the context at
28+ // every level, which is quadratic in the depth on its own.
29+ let mut root = top.field("root")?;
30+ let mut children = top.field("children")?;
31+ let mut frames: Vec<Value> = Vec::with_capacity(path.len());
32+ for idx in path {
33+ let kids = children.as_list()?;
34+ let child = kids[idx].clone();
35+ frames.push(Value::record(&[
36+ ("left", Value::list(kids[..idx].to_vec())),
37+ ("parent", root),
38+ ("right", Value::list(kids[idx + 1..].to_vec())),
39+ ]));
40+ root = child.field("root")?;
41+ children = child.field("children")?;
42+ }
43+ // innermost frame first: `up` reads context.first() as the parent
44+ frames.reverse();
45+ // `top_of` leaves no context above, but do not depend on that here
46+ frames.extend_from_slice(top.field("context")?.as_list()?);
47+ Ok(Some(Value::record(&[
48+ ("children", children),
49+ ("context", Value::list(frames)),
50+ ("root", root),
51+ ])))
52+}
53+
54+/// indices of the children to descend through to reach `id`, if it is there
55+fn find_path(children: &[Value], id: &str, path: &mut Vec<usize>) -> Result<bool, Crash> {
56+ for (i, c) in children.iter().enumerate() {
57+ path.push(i);
58+ if id_is(&c.field("root")?, id)? {
16− return Ok(Some(loc));
59+ return Ok(true);
17 }
60 }
18− let children = loc.field("children").and_then(|v| v.as_list().map(|x| x.to_vec()))?;
61+ let grandchildren = c.field("children")?;
19− // push children as refocused locations; iterate in order
62+ if find_path(grandchildren.as_list()?, id, path)? {
20− for child in children.iter() {
63+ return Ok(true);
21− stack.push(refocus_child(&loc, child)?);
22 }
64 }
65+ path.pop();
66+ }
67+ Ok(false)
68+}
69+
70+/// compare a commit's id without copying it out
71+fn id_is(commit: &Value, id: &str) -> Result<bool, Crash> {
72+ match commit.field("id")? {
73+ Value::Id(i) => Ok(i.as_str() == id),
74+ v => Err(Crash::new(format!("expected an Id, got a {}", v.kind_name()))),
23 }
75 }
24− Ok(None)
25}
76}
26
77
27fn id_of(commit: &Value) -> Result<String, Crash> {
78fn id_of(commit: &Value) -> Result<String, Crash> {
@@ -35,8 +86,10 @@ fn id_of(commit: &Value) -> Result<String, Crash> {
35fn top_of(repo: &Value) -> Result<Value, Crash> {
86fn top_of(repo: &Value) -> Result<Value, Crash> {
36 let mut cur = repo.clone();
87 let mut cur = repo.clone();
37 loop {
88 loop {
89+ // ask whether the context is empty without copying it: this runs once
90+ // per level, and copying made climbing quadratic in the depth
38− let ctx = cur.field("context").and_then(|v| v.as_list().map(|x| x.to_vec()))?;
91+ let ctx = cur.field("context")?;
39− if ctx.is_empty() {
92+ if ctx.as_list()?.is_empty() {
40 return Ok(cur);
93 return Ok(cur);
41 }
94 }
42 cur = up_of(&cur)?;
95 cur = up_of(&cur)?;
@@ -60,9 +113,14 @@ fn up_of(repo: &Value) -> Result<Value, Crash> {
60 ("children", repo.field("children")?),
113 ("children", repo.field("children")?),
61 ]));
114 ]));
62 children.extend_from_slice(right);
115 children.extend_from_slice(right);
116+ // the remaining context shares the frames rather than copying them
117+ let rest = match repo.field("context")? {
118+ Value::List(xs) => Value::List(xs.skip(1)),
119+ v => return Err(Crash::new(format!("expected a list, got a {}", v.kind_name()))),
120+ };
63 Ok(Value::record(&[
121 Ok(Value::record(&[
64 ("children", Value::list(children)),
122 ("children", Value::list(children)),
65− ("context", Value::list(ctx[1..].to_vec())),
123+ ("context", rest),
66 ("root", parent),
124 ("root", parent),
67 ]))
125 ]))
68}
126}
src/shape.rs+39−6
@@ -8,19 +8,32 @@ use std::collections::{BTreeSet, HashMap};
8pub struct Shapes {
8pub struct Shapes {
9 /// name -> its declared type
9 /// name -> its declared type
10 pub decls: HashMap<String, TypeExpr>,
10 pub decls: HashMap<String, TypeExpr>,
11+ /// Memo for `shape_of`. A contract check against a record type asks for
12+ /// the shape on every call, and computing it builds the field set afresh;
13+ /// the declarations never change once the config is loaded.
14+ memo: std::cell::RefCell<HashMap<String, Option<ShapeVal>>>,
11}
15}
12
16
13impl Shapes {
17impl Shapes {
14 pub fn new() -> Shapes {
18 pub fn new() -> Shapes {
15− Shapes {
19+ Shapes::default()
16− decls: HashMap::new(),
17− }
18 }
20 }
19
21
20 /// Compute the ShapeVal for a type name used as a value (§4.12).
22 /// Compute the ShapeVal for a type name used as a value (§4.12).
21 /// Returns None if the name is undeclared, or its shape is a function,
23 /// Returns None if the name is undeclared, or its shape is a function,
22 /// a list, or otherwise not usable.
24 /// a list, or otherwise not usable.
23 pub fn shape_of(&self, name: &str) -> Option<ShapeVal> {
25 pub fn shape_of(&self, name: &str) -> Option<ShapeVal> {
26+ if let Some(hit) = self.memo.borrow().get(name) {
27+ return hit.clone();
28+ }
29+ let computed = self.shape_of_uncached(name);
30+ self.memo
31+ .borrow_mut()
32+ .insert(name.to_string(), computed.clone());
33+ computed
34+ }
35+
36+ fn shape_of_uncached(&self, name: &str) -> Option<ShapeVal> {
24 match name {
37 match name {
25 "Int" => Some(ShapeVal {
38 "Int" => Some(ShapeVal {
26 name: name.into(),
39 name: name.into(),
@@ -73,6 +86,27 @@ impl Shapes {
73 }
86 }
74 }
87 }
75
88
89+ /// Unfold an alias chain by reference. Contracts are checked at every
90+ /// call, so cloning the type each time — a `Vec` of `String`s for a record
91+ /// type — was a per-application allocation.
92+ pub fn unfold<'a>(&'a self, ty: &'a TypeExpr) -> &'a TypeExpr {
93+ let mut cur = ty;
94+ // a declaration cycle would otherwise spin here; the bound is far
95+ // above any real alias chain
96+ for _ in 0..64 {
97+ match cur {
98+ TypeExpr::Con(n) if !is_primitive(n) && !is_type_var(n) => {
99+ match self.decls.get(n) {
100+ Some(decl) => cur = decl,
101+ None => return cur,
102+ }
103+ }
104+ _ => return cur,
105+ }
106+ }
107+ cur
108+ }
109+
76 /// Unfold an alias chain, owned, so it works with partially-populated
110 /// Unfold an alias chain, owned, so it works with partially-populated
77 /// decl maps too.
111 /// decl maps too.
78 pub fn unfold_owned(&self, ty: &TypeExpr) -> TypeExpr {
112 pub fn unfold_owned(&self, ty: &TypeExpr) -> TypeExpr {
@@ -136,8 +170,7 @@ pub fn compile_contract(shapes: &Shapes, ty: &TypeExpr) -> Contract {
136
170
137/// Check one level deep (§4.13). On failure, describe expected and got.
171/// Check one level deep (§4.13). On failure, describe expected and got.
138pub fn check(shapes: &Shapes, ty: &TypeExpr, v: &Value) -> Result<(), String> {
172pub fn check(shapes: &Shapes, ty: &TypeExpr, v: &Value) -> Result<(), String> {
139− let owned = shapes.unfold_owned(ty);
173+ let ty = shapes.unfold(ty);
140− let ty = &owned;
141 match ty {
174 match ty {
142 TypeExpr::Con(n) => {
175 TypeExpr::Con(n) => {
143 if is_type_var(n) {
176 if is_type_var(n) {
@@ -338,7 +371,7 @@ impl ContractExpr {
338 /// True if the next parameter is definitely a function type.
371 /// True if the next parameter is definitely a function type.
339 pub fn next_param_is_function(&self, shapes: &Shapes) -> bool {
372 pub fn next_param_is_function(&self, shapes: &Shapes) -> bool {
340 match self.next_param() {
373 match self.next_param() {
341− Some(t) => matches!(shapes.unfold_owned(&t), TypeExpr::Fun(_, _)),
374+ Some(t) => matches!(shapes.unfold(&t), TypeExpr::Fun(_, _)),
342 None => false,
375 None => false,
343 }
376 }
344 }
377 }
tests/laws.proptest-regressions+1
@@ -12,3 +12,4 @@ cc 6d383bd080eefe9dd1dad18f50b5778c689783bff306ad599fe8e09e6e5a36e1 # shrinks to
12cc c82f268e4e3f722b703eb4582271c0ef82a0c4f815e93742c5671b6eb5036dfd # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "p dlxs ", labels: [], files: [(["ugyox", "akvsx"], "\ny")] }, children: [GTree { root: GCommit { id: "loomukln", message: "f khli b", labels: ["dvwv"], files: [] }, children: [] }] }, [], MemBackend { metas: {"loomukln": MetaInfo { hash: "hash-loomukln", author: "A U Thor", email: "a@x", time: 1700000000 }, "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"loomukln": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"], "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": []} })
12cc c82f268e4e3f722b703eb4582271c0ef82a0c4f815e93742c5671b6eb5036dfd # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "p dlxs ", labels: [], files: [(["ugyox", "akvsx"], "\ny")] }, children: [GTree { root: GCommit { id: "loomukln", message: "f khli b", labels: ["dvwv"], files: [] }, children: [] }] }, [], MemBackend { metas: {"loomukln": MetaInfo { hash: "hash-loomukln", author: "A U Thor", email: "a@x", time: 1700000000 }, "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"loomukln": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"], "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": []} })
13cc f34a9453f280fe95b8e112bb1496878f1298798bef46af2fbea172b533b7f096 # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "", labels: [], files: [(["uz"], "\nu\nlz\n\nuvj\n\n")] }, children: [] }, [], MemBackend { metas: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": []} })
13cc f34a9453f280fe95b8e112bb1496878f1298798bef46af2fbea172b533b7f096 # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "", labels: [], files: [(["uz"], "\nu\nlz\n\nuvj\n\n")] }, children: [] }, [], MemBackend { metas: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": []} })
14cc 8b00d467e52c70d9a640a9007b734274865179eb569e837cfe264a6a59562337 # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "qoe ", labels: ["gtt"], files: [(["hx", "kdh"], "tge\n\n\n\nj\n\n\nli\n\ncc\n\nb"), (["kzn"], "\n\n\n\nl\n\n\n\n\n\n\n\n\n\nd\n")] }, children: [GTree { root: GCommit { id: "uxwrmqto", message: "lv m", labels: ["jovv"], files: [] }, children: [] }] }, [], MemBackend { metas: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }, "uxwrmqto": MetaInfo { hash: "hash-uxwrmqto", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": [], "uxwrmqto": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"]} })
14cc 8b00d467e52c70d9a640a9007b734274865179eb569e837cfe264a6a59562337 # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "qoe ", labels: ["gtt"], files: [(["hx", "kdh"], "tge\n\n\n\nj\n\n\nli\n\ncc\n\nb"), (["kzn"], "\n\n\n\nl\n\n\n\n\n\n\n\n\n\nd\n")] }, children: [GTree { root: GCommit { id: "uxwrmqto", message: "lv m", labels: ["jovv"], files: [] }, children: [] }] }, [], MemBackend { metas: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }, "uxwrmqto": MetaInfo { hash: "hash-uxwrmqto", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": [], "uxwrmqto": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"]} })
15+cc 9a84983c2b4edd8f833af86eea38f38c4dd830e5e569495ecd49b7ed00b57dc2 # shrinks to (tree, focus, backend) = (GTree { root: GCommit { id: "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", message: "", labels: [], files: [] }, children: [GTree { root: GCommit { id: "qovwurzp", message: "", labels: [], files: [] }, children: [GTree { root: GCommit { id: "ltovswzk", message: "", labels: [], files: [] }, children: [] }] }] }, [0, 0], MemBackend { metas: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": MetaInfo { hash: "hash-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", author: "A U Thor", email: "a@x", time: 1700000000 }, "ltovswzk": MetaInfo { hash: "hash-ltovswzk", author: "A U Thor", email: "a@x", time: 1700000000 }, "qovwurzp": MetaInfo { hash: "hash-qovwurzp", author: "A U Thor", email: "a@x", time: 1700000000 }}, parents: {"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz": [], "qovwurzp": ["zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"], "ltovswzk": ["qovwurzp"]}, answers_tree_queries: false, conflicts: {}, empties: {} })
tests/laws.rs+120
@@ -498,6 +498,36 @@ proptest! {
498 }
498 }
499 }
499 }
500
500
501+ #[test]
502+ fn law_by_rebuilds_the_same_zipper((tree, focus, backend) in arb_repo()) {
503+ // `by` finds the commit first and then assembles the location in one
504+ // pass, instead of refocusing at every node it visits. The location it
505+ // produces must be the one a step-by-step walk produced: same focus,
506+ // same children, and a context that still climbs back to the top.
507+ let repo = repo_value(&tree, &focus);
508+ let stored: BTreeSet<String> = backend.metas.keys().cloned().collect();
509+ let (mut i, cfg) = make_interp(backend);
510+ for id in &stored {
511+ let got = j::repo::by_id(&repo, id).unwrap();
512+ prop_assert!(got.is_some(), "by {} found nothing", id);
513+ let got = got.unwrap();
514+ // the focus is the commit asked for
515+ match got.field("root").unwrap().field("id").unwrap() {
516+ Value::Id(f) => prop_assert_eq!(&*f, id),
517+ _ => prop_assert!(false, "focus has no id"),
518+ }
519+ // `goto` through the language agrees with it
520+ let viaz = eval_fn(&mut i, &cfg, &format!("by @{}", id), repo.clone());
521+ prop_assert!(viaz.is_ok(), "by @{} failed: {:?}", id, viaz.err());
522+ prop_assert!(repos_match(&viaz.unwrap(), &got, &stored));
523+ // climbing all the way up returns the whole history unchanged
524+ let up = eval_fn(&mut i, &cfg, "top", got.clone());
525+ prop_assert!(up.is_ok(), "top failed: {:?}", up.err());
526+ let from_repo = eval_fn(&mut i, &cfg, "top", repo.clone()).unwrap();
527+ prop_assert!(repos_match(&up.unwrap(), &from_repo, &stored));
528+ }
529+ }
530+
501 #[test]
531 #[test]
502 fn law_top_top((tree, focus, backend) in arb_repo()) {
532 fn law_top_top((tree, focus, backend) in arb_repo()) {
503 let repo = repo_value(&tree, &focus);
533 let repo = repo_value(&tree, &focus);
@@ -522,3 +552,93 @@ proptest! {
522 prop_assert!(value_eq(&a, &b).unwrap());
552 prop_assert!(value_eq(&a, &b).unwrap());
523 }
553 }
524}
554}
555+
556+/// A linear repo root -> c0 -> c1 -> ... focused on the top, built directly.
557+fn linear_repo(n: usize) -> (Value, Vec<String>) {
558+ let ids: Vec<String> = (0..n).map(|k| format!("kaaa{:04}", k)).collect();
559+ let mut subtree = Value::record(&[
560+ ("children", Value::list(vec![])),
561+ (
562+ "root",
563+ Value::record(&[
564+ ("files", Value::list(vec![])),
565+ ("id", Value::Id(Rc::new(ids[n - 1].clone()))),
566+ ("labels", Value::list(vec![])),
567+ ("message", Value::text(format!("commit {}", n - 1))),
568+ ]),
569+ ),
570+ ]);
571+ for k in (0..n - 1).rev() {
572+ subtree = Value::record(&[
573+ ("children", Value::list(vec![subtree])),
574+ (
575+ "root",
576+ Value::record(&[
577+ ("files", Value::list(vec![])),
578+ ("id", Value::Id(Rc::new(ids[k].clone()))),
579+ ("labels", Value::list(vec![])),
580+ ("message", Value::text(format!("commit {}", k))),
581+ ]),
582+ ),
583+ ]);
584+ }
585+ let repo = Value::record(&[
586+ ("children", Value::list(vec![subtree])),
587+ ("context", Value::list(vec![])),
588+ (
589+ "root",
590+ Value::record(&[
591+ ("files", Value::list(vec![])),
592+ ("id", Value::Id(Rc::new(ROOT_ID.to_string()))),
593+ ("labels", Value::list(vec![])),
594+ ("message", Value::text("")),
595+ ]),
596+ ),
597+ ]);
598+ (repo, ids)
599+}
600+
601+#[test]
602+fn by_builds_the_context_innermost_first() {
603+ // `by` assembles the location in one pass now; the context must still run
604+ // from the immediate parent outwards, which is what `up` reads
605+ let (repo, ids) = linear_repo(8);
606+ for (depth, id) in ids.iter().enumerate() {
607+ let loc = j::repo::by_id(&repo, id).unwrap().expect("found");
608+ match loc.field("root").unwrap().field("id").unwrap() {
609+ Value::Id(f) => assert_eq!(&*f, id, "wrong focus"),
610+ _ => panic!("focus has no id"),
611+ }
612+ let ctx = loc.field("context").unwrap();
613+ let ctx = ctx.as_list().unwrap();
614+ assert_eq!(ctx.len(), depth + 1, "context depth for {}", id);
615+ // frame k names the ancestor k+1 levels up; the last is the root
616+ for (k, frame) in ctx.iter().enumerate() {
617+ let want = if depth >= k + 1 {
618+ ids[depth - k - 1].clone()
619+ } else {
620+ ROOT_ID.to_string()
621+ };
622+ match frame.field("parent").unwrap().field("id").unwrap() {
623+ Value::Id(p) => assert_eq!(&*p, &want, "frame {} of {}", k, id),
624+ _ => panic!("frame parent has no id"),
625+ }
626+ }
627+ }
628+}
629+
630+#[test]
631+fn by_then_climbing_back_restores_the_history() {
632+ let (repo, ids) = linear_repo(8);
633+ let (mut i, cfg) = make_interp(MemBackend::new());
634+ let whole = eval_fn(&mut i, &cfg, "top", repo.clone()).unwrap();
635+ for id in &ids {
636+ let loc = j::repo::by_id(&repo, id).unwrap().expect("found");
637+ let back = eval_fn(&mut i, &cfg, "top", loc).unwrap();
638+ assert!(
639+ value_eq(&back, &whole).unwrap_or(false),
640+ "climbing back from {} changed the history",
641+ id
642+ );
643+ }
644+}