on master help sign in
Add a native touchedPaths builtin; make status O(changed) not O(paths^2)

status took ~5s on a large repository, almost all of it in `changed`. The config's `touched` found changed paths by calling `entryAt p` — a linear scan of the snapshot — for every path in the union, an O(paths^2) interpreted loop. `touchedPaths : Change -> [Path]` is a native builtin doing one pass over the two path-keyed snapshot maps, comparing blobs by content id without reading file bytes. config.j's `touched` now delegates to it; the type and semantics are unchanged. status 5.3s -> 1.0s on the same repository. Note the builtin must be declared (`touchedPaths : Change -> [Path]`) and `touched` pointed at it in the user config as well — the reference config.j is updated. Co-authored-by: vix <290354907+vix-agent@users.noreply.github.com>

commitd1806bc633b0e227f6b633767e62dbf0ef78163a authoriko <mail@iko.soy> dateSat 19 Sep 2026, 13:02 (3 weeks ago) parent2c66fc1
3 files changed+29−4
config.j+3−3 src/builtins.rs+25 src/config.rs+1−1
config.j+3−3
@@ -141,6 +141,8 @@ splitOn : Text -> Text -> [Text] -- splitOn "/" "a/b" = ["a" "b"]
141141-- path's blob is unresolved rather than an error.
142142replay : Snapshot -> Change -> Snapshot
143143unresolved : Blob -> Bool
144+touchedPaths : Change -> [Path] -- paths whose content differs, one pass;
145+ -- blobs compared by content id, bytes never read
144146blob : Text -> Blob -- a resolved regular file
145147text : Blob -> Text -- its content (markers if unresolved)
146148by : Id -> Repo -> Repo -- focus the commit with this id; crash if absent
@@ -530,9 +532,7 @@ contentAt = \p s -> (head (entryAt p s)).content or blob ""
530532
531533-- The paths a change touches.
532534touched : Change -> [Path]
533−touched = \ch ->
534− let paths = map (.path) ch.to ++ filter (\p -> not (member p (map (.path) ch.to))) (map (.path) ch.from)
535− in filter (\p -> entryAt p ch.to /= entryAt p ch.from) paths
535+touched = touchedPaths
536536
537537-- Paths whose content differs between the focus and its parent.
538538changed : Repo -> [Path]
src/builtins.rs+25
@@ -94,6 +94,7 @@ pub fn all_builtins() -> Vec<(String, Value)> {
9494 builtin("splitOn", 2, b_split_on),
9595 builtin("replay", 2, b_replay),
9696 builtin("unresolved", 1, b_unresolved),
97+ builtin("touchedPaths", 1, b_touched_paths),
9798 builtin("blob", 1, b_blob),
9899 builtin("text", 1, b_text),
99100 builtin("by", 2, b_by),
@@ -512,6 +513,30 @@ fn b_unresolved(_i: &mut Interp, args: &[Value]) -> BResult {
512513 }
513514}
514515
516+/// the paths whose content differs between `from` and `to` snapshots (§7.3):
517+/// one pass over both path-keyed maps, comparing blobs by content id — never
518+/// reading file bytes. Replaces the quadratic `entryAt`-per-path scan.
519+fn b_touched_paths(_i: &mut Interp, args: &[Value]) -> BResult {
520+ let ch = &args[0];
521+ let from = crate::domain::snapshot_map(ch.field("from")?.as_list()?)?;
522+ let to = crate::domain::snapshot_map(ch.field("to")?.as_list()?)?;
523+ let mut paths: std::collections::BTreeSet<Vec<String>> = std::collections::BTreeSet::new();
524+ paths.extend(from.keys().cloned());
525+ paths.extend(to.keys().cloned());
526+ let mut out = Vec::new();
527+ for p in paths {
528+ let changed = match (from.get(&p), to.get(&p)) {
529+ (None, Some(_)) | (Some(_), None) => true,
530+ (Some(a), Some(b)) => !crate::value::value_eq(a, b)?,
531+ (None, None) => false,
532+ };
533+ if changed {
534+ out.push(Value::list(p.into_iter().map(Value::text).collect()));
535+ }
536+ }
537+ Ok(Value::list(out))
538+}
539+
515540fn b_blob(_i: &mut Interp, args: &[Value]) -> BResult {
516541 let t = args[0].as_text()?;
517542 Ok(Value::Blob(Rc::new(BlobVal {
src/config.rs+1−1
@@ -40,7 +40,7 @@ const RESERVED_BUILTINS: &[&str] = &[
4040 ">=", "show", "::", "map", "filter", "length", "null", "head", "tail", "last", "nth", "take",
4141 "drop", "member", "range", "foldl", "concat", "++", "startsWith", "endsWith", "splitOn",
4242 "replay", "unresolved", "blob", "text", "by", "meta", "validate", "diff", "difft", "treeWith",
43− "extract",
43+ "extract", "touchedPaths",
4444];
4545
4646pub fn is_reserved_builtin(name: &str) -> bool {