Skip to content

Fix/product zipper shared node id by factor - #136

Draft
imlvts wants to merge 4 commits into
Adam-Vandervorst:masterfrom
imlvts:fix/product-zipper-shared-node-id-by-factor
Draft

imlvts wants to merge 4 commits into
Adam-Vandervorst:masterfrom
imlvts:fix/product-zipper-shared-node-id-by-factor

Conversation

@imlvts

@imlvts imlvts commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

There's an issue with sharing ProductZipper: cannot pass node id, since the same node can be used at different levels.
This is fixed by xoring factor level into the second byte of node id. With a sanity check.
The same approach does not work for DependentProductZipper[G], since a shared node can have different next factor depending on path. Added a test in case someone wants to do it.

A secondary factor's root isn't a child of the node above it, so the
core zipper's parent lookup unwrapped None. Ask the factor's TrieRef
instead.
Brings over the follow-up from fix/product-zipper-is-shared-factor-root.

Only report sharing in the last factor, for both ProductZipper and
ProductZipperG. In an earlier factor the subtrie below a node continues
into the following factors, so the same node reached in different
factors is a different subtrie of the product. A cached cata keyed by
shared_node_id conflated the two and returned wrong results.
Replaces the last-factor-only rule. Sharing in an earlier factor is
still useful (a subtrie grafted twice within one factor is the same
subtrie of the product), but the same node reached in two different
factors is two different subtries, so its id must depend on the factor.

A node id is the node's address. In a canonical address the byte below
the top byte equals the top byte (0x00 in user space, 0xff in kernel
space), so XOR factor + 1 into that byte: the result is never a
canonical address, and no two (factor, node) pairs collide. A hash of
the pair would not do in 64 bits. An address with no room reports no
sharing, and the last factor keeps the node's own id.
Below a node, the dependent product continues into whatever enroll
returns for the path so far and the payload, so the same node reached
at two paths can root two different subtries, even within one factor.
Tagging the id with the factor, as ProductZipper now does, separates
factors but not paths, and the last factor isn't known in advance.

The new test grafts one subtrie at [1] and [2] with an enroll that picks
the next factor by the first byte; with factor-tagged ids the cached
cata reused [1]'s count at [2] and returned 4 instead of 5.

Also say at each product zipper's sharing impl why the factor's own id
would be inconsistent there.
Comment thread src/product_zipper.rs
if below_top != top {
return None;
}
Some(id ^ ((tag as u64) << 48))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We already use a crate for this which steals bits from alignment, sign, and address space?

@imlvts imlvts Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

no, we don't use a crate. there's code copied from ointers in src/trie_node.rs:2804.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

In a canonical address the byte below the top byte equals the top

No? For physical addresses you can use the top byte, but for virtual ones you can't do that, and definitely not the top two bytes.

below_top != top

0b00000001 0b00000001 is a perfectly fine start of an untagged pointer.

u8::try_from(factor + 1).ok()?;

Failing sharing on factor count seems devious.

@imlvts imlvts Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Failing sharing on factor count seems devious.

of all the limitations that PathMap and MORK have, this one should be the least controversial.
if you have more than 254 factors in a product zipper, please let me know.
doing any sort of traversal there would take too much time.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

there's code copied from ointers in ...

Hmm, why again wasn't this lifted out @luketpeterson ? Seems like we don't want to be doing this ad-hoc, a mistake like this could corrupt the trie.

doing any sort of traversal there would take too much time.

We do traversals over 1000+ bytes all the time

of all the limitations that PathMap and MORK have, this one should be the least controversial.

This should come with a big red exclamation mark. With that, we can move the <=255 assumption to optimize other methods as well perhaps.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No? For physical addresses you can use the top byte, but for virtual ones you can't do that, and definitely not the top two bytes.

I double-checked myself, and it's system-dependent. for x86, 4K tables, top 2 bytes of virtual memory are either 0x00 or 0xff.
On all systems, top 4 bits are sign-extended. I'll find a better way.

@imlvts imlvts Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

We do traversals over 1000+ bytes all the time

the limit is on the number of nested maps, not byte depth. so [map1, map2, map3, ... map254]

@adamv-symbolica adamv-symbolica Sep 24, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I double-checked myself, and it's system-dependent. for x86, 4K tables, top 2 bytes of virtual memory are either 0x00 or 0xff.

Reproduced with custom Alloc on x86, 4k tables, with 5-level paging that 0x0000_0020_0000_0000 and 0x0001_0020_0000_0000 are sent to the same node id, causing trie corruption.

the limit is on the number of nested maps

Unless enrolling factors is slow, search algorithms controlling zippers can definitely still use this, see utils int.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Converting this to draft until we have a better solution.
If shared node id is u64 and they're essentially pointers, there will be still limits on what we can do with this.

@luketpeterson luketpeterson Sep 25, 2026 •

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 strikes me as one of those "difficult in theory but solvable in practice" kind of situations.

  • We need to ensure that the factor the node is from is reflected in _id without the possibility of collisions.

We have plenty of bits to give to this use - especially because we can ditch the tags from the ODRC pointers.

So the practical problems are:

  • We need a contract that ensures that the inner zipper's _id results never use certain bits, regardless of the impl (currently nothing says _id need to be based on pointers, it's just that using pointers gets us uniqueness for free)
  • We need a contract that stops a PZ from wrapping another PZ (something that uses the bits can't then expect the bits to be available for use one level up)

Or, if there is some other really clever digest that can guarantee uniqueness without those limitations, I'm all for it. But I don't know what that would be.

@imlvts
imlvts marked this pull request as draft September 24, 2026 16:41
luketpeterson added a commit to imlvts/PathMap that referenced this pull request Sep 25, 2026
…rvative fix, but it leaves a lot of sharing on the table. This will be revisited in Adam-Vandervorst#136
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants