Skip to content

Refine meet model - #117

Open
imlvts wants to merge 6 commits into
Adam-Vandervorst:masterfrom
imlvts:fix/meet-rule
Open

imlvts wants to merge 6 commits into
Adam-Vandervorst:masterfrom
imlvts:fix/meet-rule

Conversation

@imlvts

@imlvts imlvts commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

There was an issue with meet: meet with self-node is a noop, but meet with equal node drops dangling paths.
This refines the meet spec to preserve dangling paths if they're both present.

imlvts and others added 6 commits September 16, 2026 23:25
Joining with a TinyRefNode swapped operands without inverting the identity
mask, so an unchanged destination reported COUNTER_IDENT and join_into
replaced it with the source. merge_guts and the integer pjoin also
under-reported identities, giving Element for unchanged joins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
Meets and joins keep the left operand's value on a collision, but a byte
node paired with a list node ran the operation with operands swapped and
kept the list node's values. join_k_path_into also folded k-paths in
reverse. Values now follow operand order and path order.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
prune_path walked the node stack up to find where pruning stops but left it
there when the zipper did not move, so the next write through the focus went
to the wrong node (get_val_or_set_mut panicked). Walk the stack back down.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
An empty link sharing its key with a value in the same list node carries
nothing. Subtract dropped it and reported Element for an unchanged node;
it is now Identity.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
A path survives a meet exactly when both sides have it, dangling or not,
and a value exactly when both hold one. Dense bytes that meet to nothing
stay as dangling paths, list nodes meet slot by slot keeping the shared
key prefix, and meet_into never removes its focus. With prune, dangling
paths below the focus are dropped, possibly skipping nodes shared with
the source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
meet keeps a location both sides have, dangling or not, and a value both
hold. meetPruned drops dangling paths; since pathmap may skip shared
nodes with prune, the fuzzer compares prune = false only.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019R2H8fnco29asY2v3TPbtF
@adamv-symbolica

Copy link
Copy Markdown

What's the impact for performance here (both constant, and asymptotic)? The right call may be to change the spec to care less about dangling paths.

@imlvts

imlvts commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

The current implementation of meet can have different results between map.meet(&map) and map.meet(&equal_map)). This is logically inconsistent.
The spec is blind with respect to sharing. It cannot express that at all. The only thing I can do is not to check meet this way.
The best we can do now is to leave this as a suggestion and disable meet checks in the differential fuzzer.

@imlvts

imlvts commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author
//! `meet` of a map with itself keeps a dangling path; `meet` with an equal map
//! built separately drops it.
use pathmap::PathMap;
use pathmap::zipper::*;

fn locations(m: &PathMap<u64>) -> Vec<(Vec<u8>, Option<u64>)> {
    let mut rz = m.read_zipper();
    let mut out = vec![];
    while rz.to_next_step() {
        if rz.val().is_some() || rz.child_count() == 0 { out.push((rz.path().to_vec(), rz.val().cloned())); }
    }
    out
}

fn build() -> PathMap<u64> {
    let mut map = PathMap::new();
    map.set_val_at([1], 1);
    map.create_path([0, 2, 1]);
    map.set_val_at([1, 2, 2], 1);
    map.write_zipper_at_path(&[0, 2, 1]).remove_branches(true);
    map
}

fn main() {
    let map = build();
    let same = build();
    println!("map                 {:?}", locations(&map));
    println!("map.meet(&map)      {:?}", locations(&map.meet(&map)));
    println!("map.meet(&clone)    {:?}", locations(&map.meet(&map.clone())));
    println!("map.meet(&same)     {:?}", locations(&map.meet(&same)));
    println!("map == same paths:  {}", locations(&map) == locations(&same));
    assert_eq!(locations(&map.meet(&map)), locations(&map.meet(&same)), "meet depends on node identity");
}

@Adam-Vandervorst

Copy link
Copy Markdown
Owner

"the same" is load bearing here; "equivalent up to" with "dangling paths" and "sharing" and...

@luketpeterson

Copy link
Copy Markdown
Collaborator

Yikes... It took me more than 2 weeks to write the generic meet function back in early 2025. It was one of the trickiest parts of the whole crate - and the crate has a lot of tricky parts.

I know Fable can probably do better than the un-assisted me of 18 months ago. Especially with the fuzzer to help. But hopefully we can get some more extensive benchmarks and test coverage on how well we preserve sharing too so we make sure not to lose any perf...

Anyway, I don't want to rubber stamp this PR because it's huge and hits the heart of tricky stuff. But I don't have time to spend really understanding this week.

@luketpeterson

Copy link
Copy Markdown
Collaborator

load bearing

As recently as 2025 I thought "load bearing" was clever engineer-architect-speak. Now it's a calling card of slop.

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.

4 participants