Skip to content

Focus value consistency - #163

Open
luketpeterson wants to merge 13 commits into
masterfrom
focus_value_consistency
Open

luketpeterson wants to merge 13 commits into
masterfrom
focus_value_consistency

Conversation

@luketpeterson

Copy link
Copy Markdown
Collaborator

This should be ready to go. Details about the changes in #104 issue.

Builds on #162

@imlvts Make sure I didn't screw the lean model. Changes seem like improvements / simplifications to me.

…e correct behavior has been settled for over a year and it was just cruft
Changing PathMap::restrict and WriteZipper::restrict to return None instead of Identity when the destination started empty
…nsistent behavior around zippers positioned at dangling paths

#104
…ng out tests, and updating the API to match "blind zipper" patterns.

Improving documentation and adding a diagram to illustrate behavior
#104
@luketpeterson
luketpeterson requested a review from imlvts October 10, 2026 13:30
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Differential fuzz vs Lean model: success

job log · 2026-10-28 14:18:18 UTC

WARNING: 147 new divergence(s) relative to base

crate: 20000 inputs, seed 7

agree = model and crate match; known = the divergence matches a classified bug in differential.py; new = it matches none. Only the new set is compared input by input below.

side agree known new divergences
head 19154/20000 548 298
base 6439/20000 4940 8621

130 input(s) diverge on head but not on base; 8453 diverge on base but not on head.

Known-class hits that changed

known class base head
subtract_into() drops a value under a path present in the source [subtract_drops_value] 2084 0
join_into() drops the source subtrie when the destination map is empty [join_into_empty_dst] 1486 0
AlgebraicStatus::Identity is not returned reliably when nothing changed (finding 8); reproducer in l 1159 233
graft_masked_branches() creates the focus when grafting absent branches (finding 15) [graft_child_ma 88 216
which value survives a collision follows node layout rather than the left-biased spec (join/meet and 99 66
an algebraic op keeps a dangling child the spec drops: an empty child node meets/subtracts to itself 3 12

Newly diverging inputs (head only): 130 input(s), 1 kind(s)

first differs at inputs first example
meet_k_path_into 130 random#00119: line 10

Reproducers: first 1 distinct kind(s), shrunk

random#00119: first differs at meet_k_path_into, 268 -> 37 bytes

line 10
lean: 10 meet_k_path_into ret=0 W=_ o01 e1 v92 c0 n1 f? R=0200 o0200 e1 v164 c0 n1 f00
crate: 10 meet_k_path_into ret=0 W=_ o01 e1 v- c0 n0 f? R=0200 o0200 e1 v164 c0 n1 f00

//! Generated by `pathmap_trace --repro`.  Reproduces a fuzzer input as
//! plain `pathmap` API calls.
//!
//! `wz` writes into map0 and `rz` reads map1; they are separate maps so
//! both can be held at once.  Values are `u64`.

use pathmap::PathMap;
use pathmap::utils::ByteMask;
use pathmap::zipper::*;

fn main() {
    let mut map0 = PathMap::<u64>::new();
    map0.set_val_at(&[], 0);
    map0.set_val_at(&[], 0);
    map0.set_val_at(&[0u8, 0, 0, 0, 0], 0);
    map0.set_val_at(&[0u8, 0, 0], 0);
    map0.set_val_at(&[1u8, 0], 0);
    let mut map1 = PathMap::<u64>::new();

    let mut wz = map0.write_zipper_at_path(&[]);
    let mut rz = map1.read_zipper_at_path(&[]);

    /*   0 */ if 1 != 0 && wz.child_count() != 0 { wz.meet_k_path_into(1, false); }

    drop(wz);

    // What the maps hold at the end -- compare against the trace's MAP0/MAP1.

    println!("MAP0 {:?}", map0.iter().collect::<Vec<_>>());

    println!("MAP1 {:?}", map1.iter().collect::<Vec<_>>());
}
8453 input(s) fixed on head
  • random#00000
  • random#00013
  • random#00015
  • random#00016
  • random#00017
  • random#00019
  • random#00020
  • random#00022
  • random#00027
  • random#00028
  • random#00030
  • random#00032
  • random#00034
  • random#00036
  • random#00038
  • random#00041
  • random#00043
  • random#00044
  • random#00045
  • random#00046
  • random#00047
  • random#00048
  • random#00051
  • random#00057
  • random#00060
  • random#00061
  • random#00064
  • random#00066
  • random#00068
  • random#00069
  • random#00071
  • random#00072
  • random#00076
  • random#00077
  • random#00083
  • random#00086
  • random#00087
  • random#00089
  • random#00091
  • random#00093
  • random#00094
  • random#00096
  • random#00098
  • random#00100
  • random#00103
  • random#00106
  • random#00108
  • random#00110
  • random#00111
  • random#00113

act: 5000 inputs, seed 7

agree = model and crate match; known = the divergence matches a classified bug in differential.py; new = it matches none. Only the new set is compared input by input below.

side agree known new divergences
head 3502/5000 1432 66
base 1559/5000 1528 1913

17 input(s) diverge on head but not on base; 1864 diverge on base but not on head.

Known-class hits that changed

known class base head
subtract_into() drops a value under a path present in the source [subtract_drops_value] 400 0
ACTZipper::val_count() counts from the zipper root, not the focus [act: val_count_ignores_focus] 1093 1420
which value survives a collision follows node layout rather than the left-biased spec (join/meet and 35 12

Newly diverging inputs (head only): 17 input(s), 1 kind(s)

first differs at inputs first example
meet_k_path_into 17 random#00410: line 25

Reproducers: first 1 distinct kind(s), shrunk

random#00410: first differs at meet_k_path_into, 252 -> 22 bytes (act mode: the harness reads map1 through an ArenaCompactTree; the reproducer uses a PathMap read zipper)

line 25
lean: 25 meet_k_path_into ret=0 W=_ o0000 e1 v126 c0 n1 f? R=_ o_ e1 v- c1 n1 f?
crate: 25 meet_k_path_into ret=0 W=_ o0000 e1 v- c0 n0 f? R=_ o_ e1 v- c1 n1 f?

//! Generated by `pathmap_trace --repro`.  Reproduces a fuzzer input as
//! plain `pathmap` API calls.
//!
//! `wz` writes into map0 and `rz` reads map1; they are separate maps so
//! both can be held at once.  Values are `u64`.

use pathmap::PathMap;
use pathmap::utils::ByteMask;
use pathmap::zipper::*;

fn main() {
    let mut map0 = PathMap::<u64>::new();
    let mut map1 = PathMap::<u64>::new();

    let mut wz = map0.write_zipper_at_path(&[]);
    let mut rz = map1.read_zipper_at_path(&[]);

    /*   0 */ wz.insert_prefix(&[0u8]);
    /*   1 */ wz.get_val_or_set_mut(0);
    /*   2 */ if 1 != 0 && wz.child_count() != 0 { wz.meet_k_path_into(1, false); }

    drop(wz);

    // What the maps hold at the end -- compare against the trace's MAP0/MAP1.

    println!("MAP0 {:?}", map0.iter().collect::<Vec<_>>());

    println!("MAP1 {:?}", map1.iter().collect::<Vec<_>>());
}
1864 input(s) fixed on head
  • random#00000
  • random#00008
  • random#00013
  • random#00015
  • random#00016
  • random#00017
  • random#00020
  • random#00027
  • random#00030
  • random#00035
  • random#00038
  • random#00041
  • random#00043
  • random#00044
  • random#00048
  • random#00051
  • random#00052
  • random#00054
  • random#00060
  • random#00064
  • random#00068
  • random#00070
  • random#00072
  • random#00076
  • random#00077
  • random#00086
  • random#00087
  • random#00088
  • random#00092
  • random#00093
  • random#00094
  • random#00096
  • random#00098
  • random#00099
  • random#00100
  • random#00103
  • random#00106
  • random#00107
  • random#00108
  • random#00110
  • random#00111
  • random#00113
  • random#00114
  • random#00121
  • random#00122
  • random#00124
  • random#00127
  • random#00130
  • random#00134
  • random#00135

@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Bench A/B vs base: done

job log · 2026-10-28 14:26:12 UTC

base 91abaa8 → head 75cc482, 3 round(s), median of each run averaged; negative is faster

bench cases geomean largest gain largest loss >5% faster >5% slower
shakespeare 7 -0.3% -1.6% shakespeare/shakespeare_sentences_val_count +1.1% shakespeare/shakespeare_words_val_count 0 0
cities 5 -0.8% -3.7% cities/cities_val_count +3.3% cities/cities_get 0 0
sparse_keys 108 -1.1% -12.3% sparse_subtract_after_join/8000 +7.5% sparse_zipper_prev_step_iter/800 9 4
binary_keys 89 -4.5% -22.5% binary_zipper_iter/50 +14.2% binary_get/2000 26 1
superdense_keys 116 +0.8% -18.6% superdense_insert/800 +10.4% superdense_zipper_step_iter/3200 4 12
act_paths 46 -0.9% -9.1% shape_map_to_act_cata/random_long +3.5% size_map_to_act_cata/50000 2 0
zipper_head_owned 16 -0.0% -3.6% zipper_head_owned/borrowed_head_read_missing_path +3.4% zipper_head_owned/borrowed_head_write_creation_cleanup_unchecked 0 0
product_zipper 4 -1.7% -5.0% product_zipper/introspecting_pathmap_pathmap +1.0% product_zipper/generic_act_act 1 0
59 case(s) moved more than 5%
bench case base head change
binary_keys binary_zipper_iter/50 1.44 µs 1.12 µs -22.5%
superdense_keys superdense_insert/800 17.73 µs 14.44 µs -18.6%
binary_keys binary_descend_until_max_bytes/1000 73.00 µs 60.94 µs -16.5%
binary_keys binary_descend_until/250 18.41 µs 15.41 µs -16.3%
binary_keys binary_descend_until/1000 81.51 µs 68.24 µs -16.3%
binary_keys binary_zipper_prev_iter/50 50.57 µs 43.29 µs -14.4%
binary_keys binary_get/2000 136.00 µs 155.33 µs +14.2%
binary_keys binary_zipper_iter/100 2.56 µs 2.21 µs -13.6%
binary_keys binary_zipper_prev_iter/100 97.46 µs 84.78 µs -13.0%
binary_keys binary_zipper_prev_iter/200 189.53 µs 165.33 µs -12.8%
binary_keys binary_k_path_iter/1600 39.29 µs 34.33 µs -12.6%
binary_keys binary_zipper_prev_iter/1600 1.38 ms 1.21 ms -12.4%
binary_keys binary_k_path_iter/800 20.15 µs 17.67 µs -12.3%
sparse_keys sparse_subtract_after_join/8000 122.67 µs 107.60 µs -12.3%
binary_keys binary_zipper_prev_iter/400 366.97 µs 322.40 µs -12.1%
binary_keys binary_zipper_prev_iter/800 707.83 µs 624.33 µs -11.8%
binary_keys binary_zipper_prev_step_iter/50 91.02 µs 80.48 µs -11.6%
binary_keys binary_descend_until/500 36.23 µs 32.14 µs -11.3%
sparse_keys sparse_descend_until/500 14.01 µs 12.46 µs -11.1%
binary_keys binary_zipper_prev_step_iter/100 172.93 µs 154.77 µs -10.5%
superdense_keys superdense_zipper_step_iter/3200 24.64 µs 27.21 µs +10.4%
binary_keys binary_zipper_iter/200 4.90 µs 4.44 µs -9.4%
superdense_keys superdense_zipper_step_iter/1600 13.24 µs 14.48 µs +9.3%
act_paths shape_map_to_act_cata/random_long 43.73 ms 39.75 ms -9.1%
binary_keys binary_descend_until_max_bytes/500 31.21 µs 28.40 µs -9.0%
superdense_keys superdense_zipper_step_iter/400 4.18 µs 4.55 µs +9.0%
binary_keys binary_zipper_prev_step_iter/200 329.83 µs 300.53 µs -8.9%
binary_keys binary_zipper_iter/1600 36.46 µs 33.32 µs -8.6%
binary_keys binary_zipper_iter/400 9.38 µs 8.59 µs -8.4%
sparse_keys sparse_meet/100 1.94 µs 1.78 µs -8.3%
binary_keys binary_descend_until_max_bytes/250 14.16 µs 13.02 µs -8.1%
act_paths shape_map_to_act_cata/dense_narrow 10.79 ms 9.92 ms -8.0%
binary_keys binary_zipper_prev_step_iter/400 630.43 µs 579.97 µs -8.0%
sparse_keys sparse_zipper_prev_step_iter/800 181.07 µs 194.70 µs +7.5%
superdense_keys superdense_zipper_step_iter/100 754 ns 811 ns +7.5%
superdense_keys superdense_zipper_step_iter/200 1.46 µs 1.57 µs +7.5%
superdense_keys superdense_val_count_bench_act/3200 112.23 µs 120.60 µs +7.5%
sparse_keys sparse_meet/50 941 ns 871 ns -7.4%
sparse_keys sparse_val_count_bench/125 292 ns 270 ns -7.4%
binary_keys binary_zipper_iter/800 18.12 µs 16.78 µs -7.4%
superdense_keys superdense_descend_until/1000 6.36 µs 6.81 µs +7.1%
superdense_keys superdense_val_count_bench/800 754 ns 704 ns -6.6%
superdense_keys superdense_insert/200 2.26 µs 2.12 µs -6.3%
binary_keys binary_descend_until/4000 558.50 µs 523.20 µs -6.3%
superdense_keys superdense_zipper_prev_step_iter/400 4.12 µs 4.38 µs +6.2%
superdense_keys superdense_zipper_prev_step_iter/3200 20.59 µs 21.85 µs +6.2%
superdense_keys superdense_zipper_prev_step_iter/100 607 ns 644 ns +6.1%
superdense_keys superdense_val_count_bench/400 437 ns 411 ns -6.0%
sparse_keys sparse_zipper_cursor/50 624 ns 587 ns -5.9%
sparse_keys join_sparse/50 571 ns 604 ns +5.8%
superdense_keys superdense_val_count_bench_act/800 25.40 µs 26.86 µs +5.8%
sparse_keys sparse_zipper_prev_step_iter/100 23.57 µs 22.22 µs -5.7%
binary_keys binary_k_path_iter/50 1.32 µs 1.25 µs -5.6%
sparse_keys sparse_zipper_prev_cursor/100 14.06 µs 13.28 µs -5.5%
superdense_keys superdense_descend_until/16000 115.07 µs 121.43 µs +5.5%
sparse_keys sparse_get/8000 183.33 µs 193.23 µs +5.4%
sparse_keys sparse_zipper_prev_cursor/50 6.76 µs 6.41 µs -5.2%
sparse_keys binary_drop_head/800 173.53 µs 182.40 µs +5.1%
product_zipper product_zipper/introspecting_pathmap_pathmap 473.00 ms 449.20 ms -5.0%

Full tables per bench are in the job log and the bench-out artifact.

…s the purview of #117, so it doesn't appear as a new divergence
@luketpeterson

Copy link
Copy Markdown
Collaborator Author

Full perf data attached. No detectable change in any recursive workloads (real-world cases). Degenerate cases with like joining two single-path tries together show the cost of checking the focus value (about 10%), but I don't think it's worth sacrificing to optimize for these degenerate cases, and the cost will actually be reversed (become a gain) when the new node contract is implemented.
focus_values_benchmark.zip

@Adam-Vandervorst

Copy link
Copy Markdown
Owner

Can we hold off on merging this? This touches a ton of stuff there's no benchmark for here. I have two sets of benchmarks, but need some time to integrate them.

@imlvts imlvts left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit more involved than I expected, but this certainly improves consistency.
If I'm not mistaken this also fixes one of the issues I encountered when doing algebraic equivalence, specifically because of the root/focus value.

Looks good.

@luketpeterson

luketpeterson commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator Author

This is a bit more involved than I expected, but this certainly improves consistency. If I'm not mistaken this also fixes one of the issues I encountered when doing algebraic equivalence, specifically because of the root/focus value.

Looks good.

I snuck in an API consistency fix and better documentation (both for remove_prefix). That's some of the extra bulk.

I'm holding until we get the all-clear on real-world perf.

…rune when a

lredy empty and a zero-arg was passed, and an infinite loop when already empty
# Conflicts:
#	Cargo.toml
#	src/write_zipper.rs
@imlvts

imlvts commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants