Skip to content

Make the container hash table a total order - #168

Open
jerrytron wants to merge 2 commits into
JBenda:masterfrom
choosatron:fix/container-hash-total-order
Open

Make the container hash table a total order#168
jerrytron wants to merge 2 commits into
JBenda:masterfrom
choosatron:fix/container-hash-total-order

Conversation

@jerrytron

Copy link
Copy Markdown

container_hash_t compares on the hash alone, and binary_emitter sorts that table with std::sort, which is not stable. Two entries sharing a hash can come out in either order depending on the standard library — so the same story compiles to different bytes under libstdc++ than under libc++, which makes comparing compiler output between builds useless as a correctness check.

It is not only cosmetic. story_impl.cpp's upper_bound() returns the last entry whose key is <= the target, so when entries share a hash the runtime resolves the path to whichever one the sort happened to leave last.

The ties are common, and are not hash collisions

build_container_hash_map() recurses into indexed children carrying the parent's name unchanged, so one path string is emitted once per indexed child at a different offset. One story measured here has 164 entries under 128 distinct hashes, and all 26 collisions are a repeated path rather than two different paths colliding.

This change

Tie-break on the offset, making the order total, so every toolchain agrees. key() still returns the hash alone, because the runtime's binary search looks up by hash and must keep matching every entry in a run.

Whether the highest-offset entry is the right container for a repeated path is a separate question this does not try to answer — it only makes the answer the same everywhere. After this, a story compiled by three different toolchains produced identical bytes; before, it did not.

ctest passes.

container_hash_t compared on the hash alone, and binary_emitter sorts
that table with std::sort, which is not stable. Two entries sharing a
hash could come out in either order depending on the standard library,
so the same story compiled to different bytes under libstdc++ than
under libc++ - which makes comparing output between builds useless as a
correctness check.

It is not only cosmetic. story_impl.cpp's upper_bound() returns the last
entry whose key is <= the target, so when entries share a hash the
runtime resolves the path to whichever one the sort happened to leave
last.

The ties are common and are not hash collisions between different
paths: build_container_hash_map() recurses into indexed children
carrying the parent's name unchanged, so one path string is emitted
once per indexed child at a different offset. One story measured here
has 164 entries under 128 distinct hashes, and all 26 collisions are a
repeated path.

Tie-breaking on the offset makes the order total, so every toolchain
agrees. key() still returns the hash alone, because the runtime's
binary search looks up by hash and must keep matching every entry in a
run.

Whether the highest-offset entry is the RIGHT container for a repeated
path is a separate question this does not answer; it only makes the
answer the same everywhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@JBenda

JBenda commented Sep 10, 2026

Copy link
Copy Markdown
Owner

Good spot; I changed it to references to avoid future nullptr handling. I cannot push to the branch (since it is not "your" repo), so I pushed it here: https://github.com/JBenda/inkcpp/tree/fix/defer-entry-no-copy. So may you pull it into your branch, and then we can merge.

The body fits on one line under the project's .clang-format, so the
format job wants it there. Produced with clang-format rather than by
hand; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

2 participants