Add an optional Hash impl for Tree and TreeOp - #446
Conversation
Behind a new default-off `hash` feature on `fidget-core`, passed through as `fidget/hash` on the facade. A `Tree` is a natural cache key: it fully describes the shape that will be built from it, so a consumer that compiles or meshes a tree wants to memoise on it. Without `Hash` the only options are hashing the `Debug` string -- version-fragile, and it expands shared subtrees so two equal trees can produce different keys -- or maintaining a parallel structural hash outside the crate, which then has to be kept consistent with `PartialEq` by hand. The impl is purely structural, matching `Tree`'s existing `PartialEq`, so the `Hash`/`Eq` contract holds. Two details worth review: - **Signed zero.** `+0.0 == -0.0`, so they must hash equally; `hash_f64` normalises zero before hashing bits. A naive `to_bits()` would violate the contract for a constant that is easy to produce. - **Deep trees.** The hash walks a worklist `Vec` rather than recursing, matching what `Drop` and `PartialEq` already do, so a long chain does not overflow the stack. There is a test at 100k nodes. Default-off because it commits the crate to that hashing contract. It pulls in no dependencies and costs nothing when disabled -- happy to make it unconditional instead if you would prefer that. Four tests cover the contract: equal trees hash equally, signed zero, distinct trees hash distinctly, and the deep-tree stack case.
|
I'm not going to merge this PR as-is, for two reasons:
I'm not against making trees hashable, but I don't think we want to switch to bitcasting equality in the general-purpose |
|
Alternatively, we could borrow
|
Adds
HashforTreeandTreeOpbehind a new default-offhashfeature onfidget-core, passed through asfidget/hashon the facade.Why
A
Treeis a natural cache key — it fully describes the shape that will bebuilt from it, so a consumer that compiles or meshes a tree wants to memoise on
it. Without
Hashthe options are:Debugstring — version-fragile, and it expands shared subtrees, sotwo equal trees can produce different keys; or
kept consistent with
PartialEqby hand.We've been carrying this as a fork patch and would rather not.
What
The impl is purely structural, matching
Tree's existingPartialEq, so theHash/Eqcontract holds. Two details worth your eye:+0.0 == -0.0, so they must hash equally.hash_f64normalises zero before hashing bits — a naive
to_bits()would violate thecontract for a constant that's trivially easy to produce.
Vecrather than recursing,matching what
DropandPartialEqalready do, so a long chain doesn'toverflow the stack. There's a test at 100k nodes.
Tests
Four, covering the contract rather than just the happy path:
equal_trees_hash_equallyHash/Eqcontractsigned_zero_hashes_equally+0.0/-0.0don't break itdifferent_trees_hash_differentlydeep_tree_does_not_overflow_the_stackVerified both feature states:
cargo check -p fidget-coreclean with thefeature off, and
cargo test -p fidget-core --features hashpasses 4/4.On the feature gate
Default-off because it commits the crate to the structural-hashing contract. It
pulls in no dependencies and costs nothing when disabled — happy to make it
unconditional instead if you'd prefer that; just say which you want and I'll
push the change.