on master help sign in
Stop recomputing what a run already knows; make list tails share storage
d86146biko
src/builtins.rs+13−7
@@ -341,11 +341,15 @@ fn b_head(_i: &mut Interp, args: &[Value]) -> BResult {
341}
341}
342
342
343fn b_tail(_i: &mut Interp, args: &[Value]) -> BResult {
343fn b_tail(_i: &mut Interp, args: &[Value]) -> BResult {
344− let xs = args[0].as_list()?;
344+ if args[0].as_list()?.is_empty() {
345− if xs.is_empty() {
346 return Err(Crash::new("tail: empty list"));
345 return Err(Crash::new("tail: empty list"));
347 }
346 }
348− Ok(Value::list(xs[1..].to_vec()))
347+ // share the elements rather than copying them: `up` is `tail
348+ // repo.context`, so a copying tail made every zipper walk quadratic
349+ match &args[0] {
350+ Value::List(xs) => Ok(Value::List(xs.skip(1))),
351+ v => Err(Crash::new(format!("expected a list, got a {}", v.kind_name()))),
352+ }
349}
353}
350
354
351fn b_last(_i: &mut Interp, args: &[Value]) -> BResult {
355fn b_last(_i: &mut Interp, args: &[Value]) -> BResult {
@@ -389,9 +393,11 @@ fn b_take(_i: &mut Interp, args: &[Value]) -> BResult {
389}
393}
390
394
391fn b_drop(_i: &mut Interp, args: &[Value]) -> BResult {
395fn b_drop(_i: &mut Interp, args: &[Value]) -> BResult {
392− let xs = args[1].as_list()?;
393− let n = clamp(args[0].as_int()?, xs.len())?;
396+ let n = clamp(args[0].as_int()?, args[1].as_list()?.len())?;
394− Ok(Value::list(xs[n..].to_vec()))
397+ match &args[1] {
398+ Value::List(xs) => Ok(Value::List(xs.skip(n))),
399+ v => Err(Crash::new(format!("expected a list, got a {}", v.kind_name()))),
400+ }
395}
401}
396
402
397fn b_member(_i: &mut Interp, args: &[Value]) -> BResult {
403fn b_member(_i: &mut Interp, args: &[Value]) -> BResult {
@@ -456,7 +462,7 @@ fn b_concat(_i: &mut Interp, args: &[Value]) -> BResult {
456fn b_append(_i: &mut Interp, args: &[Value]) -> BResult {
462fn b_append(_i: &mut Interp, args: &[Value]) -> BResult {
457 match (&args[0], &args[1]) {
463 match (&args[0], &args[1]) {
458 (Value::List(a), Value::List(b)) => {
464 (Value::List(a), Value::List(b)) => {
459− let mut out = a.as_ref().clone();
465+ let mut out = a.to_vec();
460 out.extend_from_slice(b);
466 out.extend_from_slice(b);
461 Ok(Value::list(out))
467 Ok(Value::list(out))
462 }
468 }
src/eval.rs+35
@@ -22,6 +22,14 @@ pub struct Interp {
22 pub current_def: RefCell<Option<String>>,
22 pub current_def: RefCell<Option<String>>,
23 /// the repository as loaded, before snapshotting (for `validate`)
23 /// the repository as loaded, before snapshotting (for `validate`)
24 pub old_repo: RefCell<Option<Value>>,
24 pub old_repo: RefCell<Option<Value>>,
25+ /// Results of applying a config revset the binary itself calls (`trunk`,
26+ /// `immutable`), keyed by the identity of the Repo record they were
27+ /// applied to. Both are pure and both are asked for more than once per
28+ /// run — `immutable` by the focus check and again by rendering or
29+ /// persisting — and each evaluation is a full interpreted walk of the
30+ /// history. The `Rc` is kept so the address cannot be reused while it is
31+ /// a key (§4.1: definitions are pure, so the result cannot change).
32+ revsets: RefCell<Vec<(&'static str, Rc<crate::value::RecordMap>, Value)>>,
25}
33}
26
34
27/// machine state
35/// machine state
@@ -170,9 +178,35 @@ impl Interp {
170 fresh: RefCell::new(0),
178 fresh: RefCell::new(0),
171 current_def: RefCell::new(None),
179 current_def: RefCell::new(None),
172 old_repo: RefCell::new(None),
180 old_repo: RefCell::new(None),
181+ revsets: RefCell::new(Vec::new()),
173 }
182 }
174 }
183 }
175
184
185+ /// Apply one of the config revsets the binary calls itself, reusing the
186+ /// result when the same Repo value has already been asked (§7.5).
187+ pub fn apply_cached_revset(&mut self, name: &'static str, repo: &Value) -> EResult {
188+ let key = match repo {
189+ Value::Record(m) => Some(m.clone()),
190+ _ => None,
191+ };
192+ if let Some(k) = &key {
193+ for (n, r, v) in self.revsets.borrow().iter() {
194+ if *n == name && Rc::ptr_eq(r, k) {
195+ return Ok(v.clone());
196+ }
197+ }
198+ }
199+ let f = self
200+ .globals
201+ .lookup(name)
202+ .ok_or_else(|| Crash::new(format!("`{}` is not defined", name)))?;
203+ let v = self.apply(f, repo.clone())?;
204+ if let Some(k) = key {
205+ self.revsets.borrow_mut().push((name, k, v.clone()));
206+ }
207+ Ok(v)
208+ }
209+
176 /// An interpreter shell used only for recursive rendering; panics if any
210 /// An interpreter shell used only for recursive rendering; panics if any
177 /// backend interaction is attempted.
211 /// backend interaction is attempted.
178 pub fn dummy() -> Interp {
212 pub fn dummy() -> Interp {
@@ -184,6 +218,7 @@ impl Interp {
184 fresh: RefCell::new(0),
218 fresh: RefCell::new(0),
185 current_def: RefCell::new(None),
219 current_def: RefCell::new(None),
186 old_repo: RefCell::new(None),
220 old_repo: RefCell::new(None),
221+ revsets: RefCell::new(Vec::new()),
187 }
222 }
188 }
223 }
189
224
src/jj.rs+8−10
@@ -413,14 +413,13 @@ impl JjBackend {
413 cfg: &Config,
413 cfg: &Config,
414 current: &Value,
414 current: &Value,
415 ) -> Result<Option<Value>, OpenError> {
415 ) -> Result<Option<Value>, OpenError> {
416− // evaluate the config to compute the immutable set
416+ // Evaluate the config to compute the immutable set. This uses the
417− let mut probe = Interp::new(
417+ // interpreter the run will go on to use, rather than a throwaway one,
418− Rc::new(self.clone()),
418+ // so that the `immutable` (and the `trunk` inside it) computed here is
419− cfg.shapes.clone(),
419+ // reused when rendering or persisting asks for the same Repo value —
420− crate::value::Env::empty(),
420+ // each is a full interpreted walk of the history.
421− );
422− crate::config::eval_config(&mut probe, cfg).map_err(|c| (3, c.msg))?;
421+ crate::config::eval_config(interp, cfg).map_err(|c| (3, c.msg))?;
423− let immutable = crate::repo::compute_immutable(&mut probe, current)
422+ let immutable = crate::repo::compute_immutable(interp, current)
424 .map_err(|c| (1, c.msg))?;
423 .map_err(|c| (1, c.msg))?;
425 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
424 let focus_id = match current.field("root").and_then(|r| r.field("id")) {
426 Ok(Value::Id(i)) => i.to_string(),
425 Ok(Value::Id(i)) => i.to_string(),
@@ -430,7 +429,6 @@ impl JjBackend {
430 return Ok(None);
429 return Ok(None);
431 }
430 }
432 // build: new empty child of the focus with the focus's files, minted id
431 // build: new empty child of the focus with the focus's files, minted id
433− let _ = interp;
434 let root = current.field("root").map_err(|c| (1, c.msg))?;
432 let root = current.field("root").map_err(|c| (1, c.msg))?;
435 let files = root.field("files").map_err(|c| (1, c.msg))?;
433 let files = root.field("files").map_err(|c| (1, c.msg))?;
436 let child = crate::value::Value::record(&[
434 let child = crate::value::Value::record(&[
@@ -439,7 +437,7 @@ impl JjBackend {
439 ("labels", crate::value::Value::list(vec![])),
437 ("labels", crate::value::Value::list(vec![])),
440 (
438 (
441 "id",
439 "id",
442− crate::value::Value::Id(Rc::new(probe.mint_id())),
440+ crate::value::Value::Id(Rc::new(interp.mint_id())),
443 ),
441 ),
444 ]);
442 ]);
445 let child_subtree = crate::value::Value::record(&[
443 let child_subtree = crate::value::Value::record(&[
src/render.rs+83−41
@@ -335,37 +335,64 @@ fn path_string(p: &Value) -> Result<String, Crash> {
335 Ok(parts.join("/"))
335 Ok(parts.join("/"))
336}
336}
337
337
338+/// entries of one snapshot, keyed by path components
339+type SnapMap = BTreeMap<Vec<String>, Value>;
340+
338fn touched_paths(change: &Value) -> Result<Vec<(String, char, bool)>, Crash> {
341fn touched_paths(change: &Value) -> Result<Vec<(String, char, bool)>, Crash> {
339− // (path, mark, unresolved)
340 let from = snapshot_map_of(&change.field("from")?)?;
342 let from = snapshot_map_of(&change.field("from")?)?;
341 let to = snapshot_map_of(&change.field("to")?)?;
343 let to = snapshot_map_of(&change.field("to")?)?;
342− let mut paths: BTreeSet<Vec<String>> = BTreeSet::new();
344+ Ok(touched_in_maps(&from, &to)?
343− paths.extend(from.keys().cloned());
345+ .into_iter()
344− paths.extend(to.keys().cloned());
346+ .map(|(p, mark, unresolved)| (p.join("/"), mark, unresolved))
347+ .collect())
348+}
349+
350+/// The changed paths between two snapshots that have already been keyed, as
351+/// (path components, mark, unresolved). Both maps are sorted, so the union is
352+/// a merge — the previous version collected every key of both sides into a
353+/// `BTreeSet`, cloning each one.
354+fn touched_in_maps(from: &SnapMap, to: &SnapMap) -> Result<Vec<(Vec<String>, char, bool)>, Crash> {
345 let mut out = Vec::new();
355 let mut out = Vec::new();
346− for p in paths {
356+ let mut fi = from.iter().peekable();
347− let f = from.get(&p);
357+ let mut ti = to.iter().peekable();
348− let t = to.get(&p);
358+ loop {
359+ let ord = match (fi.peek(), ti.peek()) {
360+ (None, None) => break,
361+ (Some(_), None) => std::cmp::Ordering::Less,
362+ (None, Some(_)) => std::cmp::Ordering::Greater,
363+ (Some((fp, _)), Some((tp, _))) => fp.cmp(tp),
364+ };
349− let mark = match (f, t) {
365+ let (path, mark, t) = match ord {
366+ std::cmp::Ordering::Less => {
367+ let (p, _) = fi.next().unwrap();
350− (None, Some(_)) => '+',
368+ (p, '−', None)
369+ }
370+ std::cmp::Ordering::Greater => {
371+ let (p, v) = ti.next().unwrap();
351− (Some(_), None) => '−',
372+ (p, '+', Some(v))
352− (Some(a), Some(b)) => {
373+ }
374+ std::cmp::Ordering::Equal => {
375+ let (p, a) = fi.next().unwrap();
376+ let (_, b) = ti.next().unwrap();
353 if crate::value::value_eq(a, b)? {
377 if crate::value::value_eq(a, b)? {
354 continue;
378 continue;
355 }
379 }
356− '~'
380+ (p, '~', Some(b))
357 }
381 }
358− (None, None) => continue,
359 };
382 };
360 let unresolved = t
383 let unresolved = t
361 .map(|b| matches!(b, Value::Blob(bl) if bl.is_unresolved()))
384 .map(|b| matches!(b, Value::Blob(bl) if bl.is_unresolved()))
362 .unwrap_or(false);
385 .unwrap_or(false);
386+ out.push((
387+ path.clone(),
363− out.push((p.join("/"), if unresolved { '✖' } else { mark }, unresolved));
388+ if unresolved { '✖' } else { mark },
389+ unresolved,
390+ ));
364 }
391 }
365 Ok(out)
392 Ok(out)
366}
393}
367
394
368−fn snapshot_map_of(snap: &Value) -> Result<BTreeMap<Vec<String>, Value>, Crash> {
395+fn snapshot_map_of(snap: &Value) -> Result<SnapMap, Crash> {
369 let mut m = BTreeMap::new();
396 let mut m = BTreeMap::new();
370 for e in snap.as_list()?.iter() {
397 for e in snap.as_list()?.iter() {
371 let path = e
398 let path = e
@@ -998,6 +1025,24 @@ impl CommitInfo {
998 }
1025 }
999}
1026}
1000
1027
1028+/// A parent commit's snapshot as seen by its children: the files value, plus
1029+/// its path-keyed map built at most once and shared by every child's diff.
1030+/// Keying a snapshot allocates a `Vec<String>` per entry, so rebuilding it for
1031+/// each child dominated `treeFull`/`treeData` on a large history.
1032+struct ParentSnap<'a> {
1033+ files: &'a Value,
1034+ map: &'a std::cell::OnceCell<SnapMap>,
1035+}
1036+
1037+impl ParentSnap<'_> {
1038+ fn map(&self) -> Result<&SnapMap, Crash> {
1039+ if self.map.get().is_none() {
1040+ let _ = self.map.set(snapshot_map_of(self.files)?);
1041+ }
1042+ Ok(self.map.get().expect("just set"))
1043+ }
1044+}
1045+
1001#[allow(clippy::too_many_arguments)]
1046#[allow(clippy::too_many_arguments)]
1002fn build_info(
1047fn build_info(
1003 interp: &mut Interp,
1048 interp: &mut Interp,
@@ -1007,7 +1052,7 @@ fn build_info(
1007 immutable: &BTreeSet<String>,
1052 immutable: &BTreeSet<String>,
1008 focus_id: &str,
1053 focus_id: &str,
1009 anc: &BTreeSet<String>,
1054 anc: &BTreeSet<String>,
1010− parent_files: Option<&Value>,
1055+ parent_files: Option<&ParentSnap<'_>>,
1011 with_focus: bool,
1056 with_focus: bool,
1012 parent_is_ancestor: bool,
1057 parent_is_ancestor: bool,
1013 parent_is_focus: bool,
1058 parent_is_focus: bool,
@@ -1049,38 +1094,39 @@ fn build_info(
1049 None
1094 None
1050 };
1095 };
1051 let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1096 let nfiles = files.as_ref().map(|f| f.as_list().map(|l| l.len())).transpose()?.unwrap_or(0);
1097+ // this commit's own snapshot map, built on first use and shared with every
1098+ // child that diffs against it
1099+ let my_map: std::cell::OnceCell<SnapMap> = std::cell::OnceCell::new();
1100+ let my_snap = files.as_ref().map(|f| ParentSnap {
1101+ files: f,
1102+ map: &my_map,
1103+ });
1052 let (empty, size, detail_marks) = match parent_files {
1104 let (empty, size, detail_marks) = match parent_files {
1053− Some(pf) => {
1105+ Some(parent) => {
1106+ let pf = parent.files;
1054 let empty = match backend_empty {
1107 let empty = match backend_empty {
1055 Some(e) => e,
1108 Some(e) => e,
1056 None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1109 None => crate::value::value_eq(files.as_ref().expect("files loaded"), pf)?,
1057 };
1110 };
1058 if want_diff {
1111 if want_diff {
1112+ // both maps are built at most once per commit and reused by
1113+ // this commit's children, so a diff costs one keying, not four
1114+ let from_map = parent.map()?;
1059− let files = files.as_ref().expect("files loaded");
1115+ let to_map = my_snap.as_ref().expect("files loaded").map()?;
1060− let change = Value::record(&[("from", pf.clone()), ("to", files.clone())]);
1116+ let marks = touched_in_maps(from_map, to_map)?;
1061− let marks = touched_paths(&change)?;
1062 // size: lines added+removed against the parent
1117 // size: lines added+removed against the parent
1063− let from_map = snapshot_map_of(pf)?;
1064− let to_map = snapshot_map_of(files)?;
1065 let mut lines = 0usize;
1118 let mut lines = 0usize;
1066− for (p, _, _) in &marks {
1119+ for (key, _, _) in &marks {
1067− let key: Vec<String> = p.split('/').map(|s| s.to_string()).collect();
1120+ let count = |m: &SnapMap| -> usize {
1068− let count = |m: &BTreeMap<Vec<String>, Value>| -> usize {
1069− match m.get(&key) {
1121+ match m.get(key) {
1070− Some(Value::Blob(b)) => {
1122+ Some(Value::Blob(b)) => b.line_count().max(1),
1071− let n = b
1072− .bytes()
1073− .map(|v| String::from_utf8_lossy(&v).lines().count())
1074− .unwrap_or(1);
1075− n.max(1)
1076− }
1077 _ => 1,
1123 _ => 1,
1078 }
1124 }
1079 };
1125 };
1080− lines += count(&from_map) + count(&to_map);
1126+ lines += count(from_map) + count(to_map);
1081 }
1127 }
1082 let marks2: Vec<(String, char)> =
1128 let marks2: Vec<(String, char)> =
1083− marks.iter().map(|(p, m, _)| (p.clone(), *m)).collect();
1129+ marks.iter().map(|(p, m, _)| (p.join("/"), *m)).collect();
1084 (marks.is_empty(), Some(lines), marks2)
1130 (marks.is_empty(), Some(lines), marks2)
1085 } else {
1131 } else {
1086 (empty, None, Vec::new())
1132 (empty, None, Vec::new())
@@ -1114,7 +1160,7 @@ fn build_info(
1114 immutable,
1160 immutable,
1115 focus_id,
1161 focus_id,
1116 anc,
1162 anc,
1117− files.as_ref(),
1163+ my_snap.as_ref(),
1118 with_focus,
1164 with_focus,
1119 is_ancestor_of_focus,
1165 is_ancestor_of_focus,
1120 is_focus_pre,
1166 is_focus_pre,
@@ -1258,11 +1304,7 @@ fn top_id(repo: &Value) -> Result<String, Crash> {
1258fn compute_trunk(interp: &mut Interp, repo: &Value) -> Result<BTreeSet<String>, Crash> {
1304fn compute_trunk(interp: &mut Interp, repo: &Value) -> Result<BTreeSet<String>, Crash> {
1259 let mut set = BTreeSet::new();
1305 let mut set = BTreeSet::new();
1260 set.insert(top_id(repo)?);
1306 set.insert(top_id(repo)?);
1261− let trunk_fn = interp
1307+ let v = interp.apply_cached_revset("trunk", repo)?;
1262− .globals
1263− .lookup("trunk")
1264− .ok_or_else(|| Crash::new("`trunk` is not defined"))?;
1265− let v = interp.apply(trunk_fn, repo.clone())?;
1266 let mut named: Vec<String> = Vec::new();
1308 let mut named: Vec<String> = Vec::new();
1267 for idv in v.as_list()?.iter() {
1309 for idv in v.as_list()?.iter() {
1268 match idv {
1310 match idv {
src/repo.rs+1−5
@@ -266,11 +266,7 @@ fn validate_labels(old: &Value, new: &Value) -> Result<(), Crash> {
266/// The immutable set: the config's `immutable` revset against `old`, plus
266/// The immutable set: the config's `immutable` revset against `old`, plus
267/// every merge commit and its ancestors (§7.5 step 3).
267/// every merge commit and its ancestors (§7.5 step 3).
268pub fn compute_immutable(i: &mut Interp, old: &Value) -> Result<BTreeSet<String>, Crash> {
268pub fn compute_immutable(i: &mut Interp, old: &Value) -> Result<BTreeSet<String>, Crash> {
269− let immutable_fn = i
269+ let v = i.apply_cached_revset("immutable", old)?;
270− .globals
271− .lookup("immutable")
272− .ok_or_else(|| Crash::new("`immutable` is not defined"))?;
273− let v = i.apply(immutable_fn, old.clone())?;
274 let mut set = BTreeSet::new();
270 let mut set = BTreeSet::new();
275 for idv in v.as_list()?.iter() {
271 for idv in v.as_list()?.iter() {
276 match idv {
272 match idv {
src/value.rs+63−3
@@ -12,7 +12,7 @@ pub enum Value {
12 Int(BigInt),
12 Int(BigInt),
13 Text(Rc<String>),
13 Text(Rc<String>),
14 Bool(bool),
14 Bool(bool),
15− List(Rc<Vec<Value>>),
15+ List(ListVal),
16 Record(Rc<RecordMap>),
16 Record(Rc<RecordMap>),
17 Fun(Rc<FunVal>),
17 Fun(Rc<FunVal>),
18 Id(Rc<String>),
18 Id(Rc<String>),
@@ -24,6 +24,44 @@ pub enum Value {
24 Thunk(Rc<ThunkVal>),
24 Thunk(Rc<ThunkVal>),
25}
25}
26
26
27+/// A list: shared elements plus the offset of its first one, so dropping a
28+/// prefix (`tail`, `drop`) shares the storage instead of copying it. The
29+/// zipper walks — `up` is `tail repo.context`, and `top`/`ancestors` apply it
30+/// once per level — made a copying `tail` quadratic in the depth of history.
31+/// A tail keeps the elements before it alive; `j` is a single short run, so
32+/// holding them costs nothing.
33+#[derive(Clone)]
34+pub struct ListVal {
35+ items: Rc<Vec<Value>>,
36+ start: usize,
37+}
38+
39+impl ListVal {
40+ pub fn new(items: Vec<Value>) -> ListVal {
41+ ListVal {
42+ items: Rc::new(items),
43+ start: 0,
44+ }
45+ }
46+ /// the list without its first `n` elements, sharing the storage
47+ pub fn skip(&self, n: usize) -> ListVal {
48+ ListVal {
49+ items: self.items.clone(),
50+ start: self.items.len().min(self.start.saturating_add(n)),
51+ }
52+ }
53+ pub fn as_slice(&self) -> &[Value] {
54+ &self.items[self.start..]
55+ }
56+}
57+
58+impl std::ops::Deref for ListVal {
59+ type Target = [Value];
60+ fn deref(&self) -> &[Value] {
61+ self.as_slice()
62+ }
63+}
64+
27pub struct ThunkVal {
65pub struct ThunkVal {
28 state: std::cell::RefCell<ThunkState>,
66 state: std::cell::RefCell<ThunkState>,
29}
67}
@@ -194,6 +232,28 @@ impl BlobVal {
194 BlobContent::Conflict(sides) => Ok(render_conflict(sides)),
232 BlobContent::Conflict(sides) => Ok(render_conflict(sides)),
195 }
233 }
196 }
234 }
235+ /// number of lines, counted over the bytes in place. Equivalent to
236+ /// `String::from_utf8_lossy(&self.bytes()?).lines().count()` — a `\n`
237+ /// byte is always a `\n` character in UTF-8, and lossy replacement never
238+ /// introduces one — but without copying the content out.
239+ pub fn line_count(&self) -> usize {
240+ fn count(b: &[u8]) -> usize {
241+ let nl = bytecount(b);
242+ if b.is_empty() || b.ends_with(b"\n") {
243+ nl
244+ } else {
245+ nl + 1
246+ }
247+ }
248+ fn bytecount(b: &[u8]) -> usize {
249+ b.iter().filter(|c| **c == b'\n').count()
250+ }
251+ match &self.content {
252+ BlobContent::Resolved(b) => count(b),
253+ BlobContent::Lazy(l) => l.force().map(|b| count(&b)).unwrap_or(1),
254+ BlobContent::Conflict(sides) => count(&render_conflict(sides)),
255+ }
256+ }
197 /// the content hash of a lazy blob, if it is one (used to persist an
257 /// the content hash of a lazy blob, if it is one (used to persist an
198 /// unchanged blob without reading its bytes)
258 /// unchanged blob without reading its bytes)
199 pub fn lazy_id(&self) -> Option<&str> {
259 pub fn lazy_id(&self) -> Option<&str> {
@@ -364,7 +424,7 @@ impl Value {
364
424
365 pub fn as_list(&self) -> Result<&[Value], Crash> {
425 pub fn as_list(&self) -> Result<&[Value], Crash> {
366 match self {
426 match self {
367− Value::List(xs) => Ok(xs),
427+ Value::List(xs) => Ok(xs.as_slice()),
368 _ => Err(Crash::new(format!("expected a list, got a {}", self.kind_name()))),
428 _ => Err(Crash::new(format!("expected a list, got a {}", self.kind_name()))),
369 }
429 }
370 }
430 }
@@ -392,7 +452,7 @@ impl Value {
392 }
452 }
393
453
394 pub fn list(xs: Vec<Value>) -> Value {
454 pub fn list(xs: Vec<Value>) -> Value {
395− Value::List(Rc::new(xs))
455+ Value::List(ListVal::new(xs))
396 }
456 }
397
457
398 pub fn bool(b: bool) -> Value {
458 pub fn bool(b: bool) -> Value {
tests/builtins.rs+36
@@ -302,3 +302,39 @@ fn boolean_operators_as_values() {
302 check!(i, cfg, "false && (crash \"boom\")", Value::Bool(false));
302 check!(i, cfg, "false && (crash \"boom\")", Value::Bool(false));
303 check!(i, cfg, "true || (crash \"boom\")", Value::Bool(true));
303 check!(i, cfg, "true || (crash \"boom\")", Value::Bool(true));
304}
304}
305+
306+#[test]
307+fn tail_and_drop_share_storage_without_changing_semantics() {
308+ // `tail`/`drop` now return a view over the same elements instead of
309+ // copying them (a copying `tail` made every zipper walk quadratic, since
310+ // `up` is `tail repo.context`). The results must still be ordinary lists.
311+ let (mut i, cfg) = make_interp();
312+ check!(i, cfg, "tail [1 2 3]", ints(&[2, 3]));
313+ check!(i, cfg, "tail (tail [1 2 3])", ints(&[3]));
314+ check!(i, cfg, "tail (tail (tail [1 2 3]))", ints(&[]));
315+ check!(i, cfg, "drop 2 [1 2 3 4]", ints(&[3, 4]));
316+ check!(i, cfg, "drop 0 [1 2]", ints(&[1, 2]));
317+ check!(i, cfg, "drop 99 [1 2]", ints(&[]));
318+ check!(i, cfg, "drop 1 (drop 1 [1 2 3 4])", ints(&[3, 4]));
319+ // a view is equal to the same list built directly
320+ check!(i, cfg, "tail [1 2 3] == [2 3]", Value::Bool(true));
321+ check!(i, cfg, "drop 1 (tail [1 2 3 4]) == [3 4]", Value::Bool(true));
322+ // and behaves like one everywhere else
323+ check!(i, cfg, "length (tail [1 2 3])", Value::int(2));
324+ check!(i, cfg, "head (tail [1 2 3])", Value::int(2));
325+ check!(i, cfg, "last (tail [1 2 3])", Value::int(3));
326+ check!(i, cfg, "nth 0 (tail [1 2 3])", Value::int(2));
327+ check!(i, cfg, "tail [1 2 3] ++ [9]", ints(&[2, 3, 9]));
328+ check!(i, cfg, "0 :: tail [1 2 3]", ints(&[0, 2, 3]));
329+ check!(i, cfg, "take 1 (tail [1 2 3])", ints(&[2]));
330+ check!(i, cfg, "map (\\x -> x * 2) (tail [1 2 3])", ints(&[4, 6]));
331+ check!(i, cfg, "member 2 (tail [1 2 3])", Value::Bool(true));
332+ check!(i, cfg, "member 1 (tail [1 2 3])", Value::Bool(false));
333+ check!(i, cfg, "concat [(tail [1 2]) (tail [3 4])]", ints(&[2, 4]));
334+ check!(i, cfg, "null (tail [1])", Value::Bool(true));
335+ // the original is untouched by taking a tail of it
336+ check!(i, cfg, "let xs = [1 2 3] in tail xs ++ xs", ints(&[2, 3, 1, 2, 3]));
337+ // show round-trips a view
338+ check!(i, cfg, "show (tail [1 2 3])", Value::text("[2 3]"));
339+ assert!(crash(&mut i, &cfg, "tail []").contains("empty"));
340+}
tests/laws.rs+21
@@ -477,6 +477,27 @@ proptest! {
477 prop_assert!(repos_match(&got.unwrap(), &repo, &stored));
477 prop_assert!(repos_match(&got.unwrap(), &repo, &stored));
478 }
478 }
479
479
480+ #[test]
481+ fn law_cached_revset_matches_direct((tree, focus, backend) in arb_repo()) {
482+ // `trunk` and `immutable` are memoised per Repo value, because the
483+ // binary asks for each more than once per run and every evaluation
484+ // walks the whole history. The cached answer must be the answer the
485+ // config gives, and must not change on a second ask.
486+ let repo = repo_value(&tree, &focus);
487+ let (mut i, cfg) = make_interp(backend);
488+ for name in ["trunk", "immutable"] {
489+ let direct = eval_fn(&mut i, &cfg, name, repo.clone());
490+ prop_assert!(direct.is_ok(), "{} failed: {:?}", name, direct.err());
491+ let direct = direct.unwrap();
492+ let first = i.apply_cached_revset(name, &repo);
493+ prop_assert!(first.is_ok(), "cached {} failed", name);
494+ let first = first.unwrap();
495+ let second = i.apply_cached_revset(name, &repo).unwrap();
496+ prop_assert!(value_eq(&direct, &first).unwrap_or(false), "{}: cached differs", name);
497+ prop_assert!(value_eq(&first, &second).unwrap_or(false), "{}: second ask differs", name);
498+ }
499+ }
500+
480 #[test]
501 #[test]
481 fn law_top_top((tree, focus, backend) in arb_repo()) {
502 fn law_top_top((tree, focus, backend) in arb_repo()) {
482 let repo = repo_value(&tree, &focus);
503 let repo = repo_value(&tree, &focus);