diff --git a/pathmap-book/src/1.01.01_algebraic_traits.md b/pathmap-book/src/1.01.01_algebraic_traits.md deleted file mode 100644 index dccdb6e9..00000000 --- a/pathmap-book/src/1.01.01_algebraic_traits.md +++ /dev/null @@ -1,6 +0,0 @@ -# Lattice and DistributiveLattice trait -The `Lattice` trait and related traits in the `ring` module are used to define `join` and `meet` behaviors on values within a `PathMap`. So two values at the same path may interact with each other as part of these operations. - -WARNING: This mechanism is planned for rework, to allow a "policy" to be provided instead of forcing a `Lattice` implementation to be associated to the value type. Therefore the current behaviors will not be explained further. - -{{#include api_links.md}} diff --git a/pathmap-book/src/1.01.01_trie_and_value_lattice.md b/pathmap-book/src/1.01.01_trie_and_value_lattice.md new file mode 100644 index 00000000..2319fe7e --- /dev/null +++ b/pathmap-book/src/1.01.01_trie_and_value_lattice.md @@ -0,0 +1,49 @@ +# Trie and Value Lattices + +Algebraic operations in [PathMap] combine rules at two levels. At the outer level, lattices over tries and subtries describe relationships between trie structures. At the inner level, where values occupy corresponding locations in the tries, value-specific lattice rules describe how those values interact. + +This behavior is usually intuitive, but there are edge cases to understand, concerning empty subtries, absent values, and present values that represent a bottom element. + +## Structural Relations Between Tries + +Tries are ordered by structural containment. One trie is below another when all of its structure occurs in the other. Join is the least upper bound, meet is the greatest lower bound, and the empty trie is the bottom element. + +A whole [`PathMap`] is one trie in this ordering. The same relationships apply to every subtrie, including a subtrie exposed through a zipper. + +Trie structure is independent of stored values. A path may exist without a value. A dangling path may exist without a value or child. Both still participate in structural operations. + +## Value Interactions at Collisions + +Value lattice rules define how values relate and how an operation combines them. The trie applies these rules when multiple operands contain values at the same location. + +Different value policies may define a different ordering over the same value type. A join policy may define a join-semilattice, while a meet policy may define a meet-semilattice. They form one lattice only when they describe the same order and satisfy the compatibility laws. The API does not assume this. + +Value orderings need not resemble trie containment. For example, permission sets might use union for join and intersection for meet. If two tries store permission sets at `users:ada`, joining the tries also joins those values according to the value policy. But If only one trie stores a value at `users:lin`, that value will be included in the resulting trie without invoking the value policy's function. + +## Absence Is Not a Bottom Value + +An absent value is different from a present bottom value. When a value is not present at a path, algebraic operations on the outer lattice will often bypass the value policy entirely. A stored bottom value, however, will cause the policy to be invoked when appropriate. This is true even when if the bottom value has an application-level meaning is equivalent to "empty." + +For example, joining two locations without values leaves no stored value and does not invoke the policy. But joining two stored `Option::None` values invokes the polcity and produces a new `Option::None` value. A join of present operands must produce an upper bound, so it cannot turn present values into structural absence. + +## Composition Rules for the Trie Level and Value Level + +A generic trie operation can be conceptualized as two steps: + +1. Apply the structural operation to determine which paths and subtries belong in the result. +2. Apply the value operation where values coincide. + +The implementation interleaves these steps for efficiency. Structural guarantees allow it to reuse or skip entire subtries. It invokes value rules only where values interact. + + +GOAT: +* TrieLattice policies and ValueLattice policies are separate objects +* The trie policy decides when and how the value policy is called, and even whether it's called +* Therefore it makes sense to make the ValuePolicy a generic argument to the object that implements the TriePolicy +* Of course we can have shortcuts so the caller doesn't need to say `alg_op::>>()` + +* The open question is how we slice the implementation. If we make the impl generic enough, we will limit flexibility by trying to express limitations like direction constraints (up, down, unconstrained) in the types. But also the mind-melting logic that is generic meet will certainly need some implementation-provided scaffolding. And therefore how much of that scaffolding will need to know about the nature of the policies to be optimal?? +* I don't think I'm going to be confident in the answer to that until I actually do the implementation for real. + + +{{#include api_links.md}} diff --git a/pathmap-book/src/1.01.02_algebraic_traits.md b/pathmap-book/src/1.01.02_algebraic_traits.md new file mode 100644 index 00000000..4be79425 --- /dev/null +++ b/pathmap-book/src/1.01.02_algebraic_traits.md @@ -0,0 +1,106 @@ +# [`Lattice`] and [`DistributiveLattice`] Traits + +The previous section introduced structural lattice operations on tries and the operation-specific rules applied to values. This section describes the current traits and result types, followed by the planned policy model for value-level operations. + +The [`Lattice`] trait and related traits in the [`ring`] module are used to define [`join`] and [`meet`] behaviors on values within a [`PathMap`]. So two values at the same path may interact with each other as part of these operations. + +WARNING: This mechanism is planned for rework, to allow a "policy" to be provided instead of forcing a [`Lattice`] implementation to be associated to the value type. Therefore the current API entry points will not be explained further. + +## Planned Policy Model (for value operations) + +The planned API separates algebraic operations along two axes, creating 6 possible policy types. The type describes the guarantees that the implementation may rely upon; a policy remains responsible for defining the operation's particular value semantics. + +### Axes to Classify Operations + +The first axis describes the roles of the operands: + +- **Symmetric:** Operand positions are interchangeable. These operations support equivalent two-operand and multi-operand forms; their policy contract includes the associativity needed to fold multiple operands, in addition to permutation symmetry. Join and meet are symmetric. +- **Positional:** Each operand has a distinct role, and exchanging operands may change the result. A general multi-operand fold is therefore not implied. Subtract is positional. + +The second axis describes how the result may move through the ordering defined by the policy: + +- **Upward:** The result is greater than or equal to the relevant input operands. An operation invoked with present operands cannot remove the value. Join is upward. +- **Downward:** The result is less than or equal to the relevant input operands and may remove the value. Meet and subtract are downward. +- **Unconstrained:** The result may move either upward or downward. The trie cannot apply optimizations that depend upon a known direction of movement. + +For a symmetric operation, "relevant input operands" means every operand. For a positional operation, the ordering guarantee is relative to the primary (left or `self`) operand unless the policy documents a stronger guarantee. For example, subtraction is downward from its left operand, but its result need not be below its right operand. + +### Table of Ops + +| Movement | Symmetric | Positional | +| --- | --- | --- | +| Upward | Join | future policy-defined op | +| Downward | Meet | Subtract, ^restrict | +| Unconstrained | future policy-defined op | future policy-defined op | + +These guarantees also constrain valid operation results. An upward value-level operation cannot report that no value remains. A symmetric operation may identify any equivalent operand, including multiple operands in an n-ary operation. A positional operation preserves operand roles, so any identity information must identify an operand that the policy permits the caller to reuse. In practice this probably means only the first operand. + +GOAT: ^restrict doesn't make much sense as a value policy. + +## Algebraic Operation Results + +[`AlgebraicResult`] reports whether the operation produced no element ([`None`][`AlgebraicResult::None`]), a result equal to one or more operands ([`Identity`][`AlgebraicResult::Identity`]), or an element that must be returned ([`Element`][`AlgebraicResult::Element`]). Joining supplied elements always produces an element, even when that element is the bottom under the applicable value ordering. At the structural level, however, joining two absent operands produces no element; the value-level join operation is not called. + +For subtraction and restriction, only the left operand can be an identity. These operations are positional, so equality with the right operand in a degenerate case does not make the right operand interchangeable with the left. + +[`AlgebraicStatus`] expresses the corresponding outcome for an operation performed in place. [`Identity`][`AlgebraicStatus::Identity`] means the left operand was unchanged, [`Element`][`AlgebraicStatus::Element`] means it now contains the result without making the stronger unchanged claim, and [`None`][`AlgebraicStatus::None`] means that no result remains. Some facts can overlap: if the left operand was already empty and remains empty, both [`Identity`][`AlgebraicStatus::Identity`] and [`None`][`AlgebraicStatus::None`] describe part of what happened. + +## Result Cases (for binary ops) + +Let `A` be the left (`self`) operand, `B` the right (`other`) operand, and `R` the result. The tables use set notation for the lattice order: `A ⊆ B` means that all information represented by `A` is also represented by `B`, and `∅` denotes the empty or bottom element. For the structural part of a `PathMap`, this can be read as ordinary inclusion between sets of paths. + +### Union (aka join), `A ∪ B` + +| Case | Result | [`AlgebraicResult`] | +| --- | --- | --- | +| `B ⊂ A` | `R = A` | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `A ⊂ B` | `R = B` | [`Identity(COUNTER_IDENT)`][`AlgebraicResult::Identity`] | +| `A = B ≠ ∅` | `R = A = B`; the result equals both operands | [`Identity(SELF_IDENT \| COUNTER_IDENT)`][`AlgebraicResult::Identity`] | +| `A ⊈ B` and `B ⊈ A` | `R` is a new, nonempty result containing information from both operands | [`Element`][`AlgebraicResult::Element`] | +| `A = B = ∅` | `R = ∅` | [`None`][`AlgebraicResult::None`] | + +### Intersect (aka meet), `A ∩ B` + +| Case | Result | [`AlgebraicResult`] | +| --- | --- | --- | +| `A ⊂ B` and `A ≠ ∅` | `R = A`; the result is the smaller operand | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `B ⊂ A` and `B ≠ ∅` | `R = B`; the result is the smaller operand | [`Identity(COUNTER_IDENT)`][`AlgebraicResult::Identity`] | +| `A = B ≠ ∅` | `R = A = B`; the result equals both operands | [`Identity(SELF_IDENT \| COUNTER_IDENT)`][`AlgebraicResult::Identity`] | +| `A ∩ B ≠ ∅`, `A ⊈ B`, and `B ⊈ A` | `R` is a new, nonempty result containing information common to both operands | [`Element`][`AlgebraicResult::Element`] | +| `A ≠ ∅`, `B ≠ ∅`, and `A ∩ B = ∅` | `R = ∅` | [`None`][`AlgebraicResult::None`] | +| `A = ∅` or `B = ∅` | `R = ∅` | [`None`][`AlgebraicResult::None`] | + +### Subtract, `A ∖ B` + +| Case | Result | [`AlgebraicResult`] | +| --- | --- | --- | +| `A ≠ ∅` and `A ∩ B = ∅` | `R = A` | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `A ∩ B ≠ ∅` and `A ⊈ B` | `R` is a new, nonempty result containing the information in `A` that is not in `B` | [`Element`][`AlgebraicResult::Element`] | +| `A = B` | `R = ∅`; an operand subtracts completely from itself | [`None`][`AlgebraicResult::None`] | +| `A ⊆ B` | `R = ∅`; this is the general case of `A = B` | [`None`][`AlgebraicResult::None`] | +| `A = ∅` | `R = ∅` | [`None`][`AlgebraicResult::None`] | + +For these operations, values at coincident paths are combined according to their own trait implementations, so their behavior may add another reason for a whole-map result to differ from either operand. + +### Restrict + +Restriction is currently a separate operation, but is conceptually a policy layered on meet. It keeps a path from `A` exactly when that path has a prefix carrying a value in `B`: + +`R = { a ∈ A | some b ∈ B is a prefix of a }` + +The phrase “prefix-covers `B`” below means that every path in `B` is a prefix of at least one path in `R`. This is a prefix relationship, not ordinary set containment. The rows describing a nonempty result can overlap; the refinements state whether restriction also dropped anything from `A`. + +| Case | Result | [`AlgebraicResult`] | +| --- | --- | --- | +| `A ≠ ∅` and every path in `A` is prefixed by a valued path in `B` | `R = A`; no path from `A` was dropped | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `R = B ≠ ∅` and `A = B` | The result equals both operands; no path from `A` was dropped | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `R = B ≠ ∅` and `A ≠ B` | The result equals `B`; the paths in `A ∖ B` were dropped | [`Element`][`AlgebraicResult::Element`] | +| `R` prefix-covers `B`, `R ≠ B`, and `R = A` | `R` is a prefix-superset of `B`; no path from `A` was dropped | [`Identity(SELF_IDENT)`][`AlgebraicResult::Identity`] | +| `R` prefix-covers `B`, `R ≠ B`, and `R ≠ A` | `R` is a prefix-superset of `B`; the paths in `A ∖ R` were dropped | [`Element`][`AlgebraicResult::Element`] | +| Some, but not all, paths in `A` are prefixed by paths in `B`, and some paths in `B` serve as no prefix | `R` is a bespoke nonempty result; the unprefixed paths in `A` were dropped | [`Element`][`AlgebraicResult::Element`] | +| `A = ∅` and `B = ∅` | `R = ∅`; both operands were empty | [`None`][`AlgebraicResult::None`] | +| `A = ∅` and `B ≠ ∅` | `R = ∅`; only the left operand was empty | [`None`][`AlgebraicResult::None`] | +| `A ≠ ∅` and `B = ∅` | `R = ∅`; only the right operand was empty, so every path in `A` was dropped | [`None`][`AlgebraicResult::None`] | +| `A ≠ ∅` and `B ≠ ∅`, but no path in `A` is prefixed by a path in `B` | `R = ∅`; both operands were nonempty, but every path in `A` was dropped | [`None`][`AlgebraicResult::None`] | + +{{#include api_links.md}} diff --git a/pathmap-book/src/SUMMARY.md b/pathmap-book/src/SUMMARY.md index a4efe673..438391bd 100644 --- a/pathmap-book/src/SUMMARY.md +++ b/pathmap-book/src/SUMMARY.md @@ -5,7 +5,8 @@ - [PathMap Intro](./1.00.00_intro.md) - [Basic Structure](./1.00.01_basics.md) - [Algebraic Operations](./1.01.00_algebraic_ops.md) - - [Traits and Values](./1.01.01_algebraic_traits.md) + - [Trie and Value Lattices](./1.01.01_trie_and_value_lattice.md) + - [Traits and Values](./1.01.02_algebraic_traits.md) - [Zippers](./1.02.00_zippers.md) - [Base Trait](./1.02.01_zipper_trait.md) - [Value Access](./1.02.02_zipper_values.md) diff --git a/pathmap-book/src/api_links.md b/pathmap-book/src/api_links.md index 877a488a..d05828c6 100644 --- a/pathmap-book/src/api_links.md +++ b/pathmap-book/src/api_links.md @@ -99,6 +99,17 @@ [`join_into_take`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperWriting.html#tymethod.join_into_take [`join_into`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperWriting.html#tymethod.join_into [`join_k_path_into`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperWriting.html#tymethod.join_k_path_into +[`ring`]: https://docs.rs/pathmap/latest/pathmap/ring/index.html +[`Lattice`]: https://docs.rs/pathmap/latest/pathmap/ring/trait.Lattice.html +[`DistributiveLattice`]: https://docs.rs/pathmap/latest/pathmap/ring/trait.DistributiveLattice.html +[`AlgebraicResult`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicResult.html +[`AlgebraicResult::None`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicResult.html#variant.None +[`AlgebraicResult::Identity`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicResult.html#variant.Identity +[`AlgebraicResult::Element`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicResult.html#variant.Element +[`AlgebraicStatus`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicStatus.html +[`AlgebraicStatus::None`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicStatus.html#variant.None +[`AlgebraicStatus::Identity`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicStatus.html#variant.Identity +[`AlgebraicStatus::Element`]: https://docs.rs/pathmap/latest/pathmap/ring/enum.AlgebraicStatus.html#variant.Element [`join_map`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperWriting.html#tymethod.join_map [`make_map`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperSubtries.html#tymethod.make_map [`meet_2`]: https://docs.rs/pathmap/latest/pathmap/zipper/trait.ZipperWriting.html#tymethod.meet_2 diff --git a/src/dense_byte_node.rs b/src/dense_byte_node.rs index bce118e2..80b5fd2c 100644 --- a/src/dense_byte_node.rs +++ b/src/dense_byte_node.rs @@ -1857,7 +1857,11 @@ impl, Other }; let val_status = match self.val_mut() { Some(self_val) => match other_val { - Some(other_val) => self_val.join_into(other_val), + Some(other_val) => { + let status = self_val.join_into(other_val); + debug_assert!(!status.is_none(), "Lattice::join_into returned None for a join"); + status + }, None => AlgebraicStatus::Identity, }, None => match other_val { diff --git a/src/line_list_node.rs b/src/line_list_node.rs index f0ffc493..596cfa1e 100644 --- a/src/line_list_node.rs +++ b/src/line_list_node.rs @@ -1330,7 +1330,9 @@ fn merge_guts<'a, V: Clone + Lattice + Send + Sync, A: Allocator, const ASLOT: u (false, false) => { //both are values, so join them let a_val = unsafe{ a.val_in_slot::() }; let b_val = unsafe{ b.val_in_slot::() }; - return a_val.pjoin(b_val).map(|new_val| (a_key, ValOrChild::Val(new_val))) + let result = a_val.pjoin(b_val); + debug_assert!(!result.is_none(), "Lattice::pjoin returned None for a join"); + return result.map(|new_val| (a_key, ValOrChild::Val(new_val))) }, _ => {} } @@ -2773,10 +2775,10 @@ impl TrieNode for LineListNode let payload1 = temp_node.take_payload::<1>().unwrap(); let payload0 = temp_node.take_payload::<0>().unwrap(); let merged = match (payload0, payload1) { - (ValOrChild::Val(v0), ValOrChild::Val(v1)) => match v0.pjoin(&v1) { - AlgebraicResult::Element(v) => Some(ValOrChild::Val(v)), - AlgebraicResult::Identity(mask) => Some(ValOrChild::Val(if mask & SELF_IDENT > 0 { v0 } else { v1 })), - AlgebraicResult::None => None, + (ValOrChild::Val(mut v0), ValOrChild::Val(v1)) => { + let status = v0.join_into(v1); + debug_assert!(!status.is_none(), "Lattice::join_into returned None for a join"); + Some(ValOrChild::Val(v0)) }, (ValOrChild::Child(c0), ValOrChild::Child(c1)) => match c0.pjoin(&c1) { AlgebraicResult::Element(c) => Some(ValOrChild::Child(c)), @@ -2996,12 +2998,13 @@ pub(crate) fn validate_node(node: &LineLis panic!() } - //Two slots may share a key (that is how a value and the onward child at the same path are - // stored) but only one of them may be the onward child, otherwise the byte leads to two - // different subtries and every accessor is free to pick a different one - if node.is_used_child_0() && node.is_used_child_1() && key0 == key1 { - println!("Invalid node - two onward children under the same key. {node:?}"); - panic!() + //Two slots may share a key only for the legal value-and-onward-child representation. Two values + // make iteration see the path once while val_count sees it twice; two children make accessors free + // to choose different subtries. + if node.is_used::<1>() && key0 == key1 { + assert_eq!(key0.len(), 1, "Invalid node - Identical keys with >1 byte overlap {node:?}"); + assert_eq!(key1.len(), 1, "Invalid node - Identical keys with >1 byte overlap {node:?}"); + assert_ne!(node.is_child_ptr::<0>(), node.is_child_ptr::<1>(), "Invalid node - duplicate payload kind under the same key. {node:?}"); } // If two unequal keys share a prefix but neither is an ancestor of the diff --git a/src/ring.rs b/src/ring.rs index 4b9b1d45..3c5e4525 100644 --- a/src/ring.rs +++ b/src/ring.rs @@ -2,33 +2,39 @@ use std::collections::{HashMap, HashSet}; use std::hash::Hash; -/// The result of an algebraic operation on elements in a partial lattice +/// The result of an algebraic operation on elements in a partial lattice. /// -/// NOTE: For some operations, it is conceptually valid for both `Identity` and `None` results to be +/// For some operations, it is conceptually valid for both `Identity` and `None` results to be /// simultaneously appropriate, for example `None.pmeet(Some)`. In these situations, `None` should take precedence /// over `Identity`, but either of the results can be considered correct so your code must behave correctly in /// either case. -/// -/// NOTE 2: The following conditions for the Identity bitmask must be respected or the implementation may panic or -/// produce logically invalid results. -/// - The bit mask must be non-zero -/// - Bits beyond the number of operation arguments must not be set. e.g. an arity-2 operation may only set bit 0 -/// and bit 1, but never any additional bits. -/// - Setting two or more bits simultaneously asserts the arguments are identities of each other, so this must be -/// true in fact. -/// - The inverse of the above does not hold. E.g. if multiple bits are not set, it may **not** be assumed that -/// the arguments are not identities of each other. -/// - Non-commutative operations, such as [DistributiveLattice::psubtract], must never set bits beyond bit 0 ([SELF_IDENT]) -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum AlgebraicResult { - /// A result indicating the input values perfectly annhilate and the output should be removed and discarded - #[default] + /// No output element remains, so any stored output should be removed. + /// + /// This is not a representation of a lattice's bottom element. In particular, a join of supplied + /// elements must not return `None`, because join is an upper bound in the lattice, and therefore exists. None, /// A result indicating the output element is identical to the input element(s) identified by the bit mask /// - /// NOTE: The constants [SELF_IDENT] and [COUNTER_IDENT] can be used as conveniences when specifying the bitmask. + /// The constants [SELF_IDENT] and [COUNTER_IDENT] can be used as conveniences when specifying the bitmask. + /// + /// The following conditions for the `Identity` bitmask must be respected or the implementation may + /// panic or produce logically invalid results. + /// + /// - The bit mask must be non-zero + /// - Bits beyond the number of operation arguments must not be set. e.g. an arity-2 operation may only set bit 0 + /// and bit 1, but never any additional bits. + /// - Setting two or more bits simultaneously asserts the arguments are identities of each other, so this must be + /// true in fact. + /// - The inverse of the above does not hold. E.g. if multiple bits are not set, it may **not** be assumed that + /// the arguments are not identities of each other. + /// - Non-commutative operations, such as [DistributiveLattice::psubtract], must never set bits beyond bit 0 ([SELF_IDENT]) Identity(u64), - /// A new result element + /// The operation returns this output element rather than reusing an input. + /// + /// This variant makes no inequality claim: an implementation may return `Element` even when the + /// value happens to equal an input, although this is discouraged and will lead to performance loss. Element(V), } @@ -320,29 +326,38 @@ impl AlgebraicResult> { } } -/// Status result that is returned from an in-place algebraic operation (a method that takes `&mut self`) +/// Status returned from an in-place algebraic operation (a method that takes `&mut self`). /// -/// NOTE: `AlgebraicStatus` values are ordered, with `Element` being the lowest value and `None` being the -/// highest. Higher values make stronger guarantees about the results of the operation, but a lower values -/// are still correct and your code must behave appropriately. +/// For some operations, it is conceptually valid for multiple results to be simultaneously appropriate. +/// `None` is considered the "strongest" status, while `Identity` is a stronger status than `Element`. +/// Implementations should return the strongest valid status. /// -/// For example, for example `Empty.join(Empty)` would result in Empty, but also leave the original value -/// unmodified, therefore both `Identity` and `None` are conceptually valid in that case. +/// For example the status of: +/// * `None.meet_into(Some)` could be correctly described as both `Identity` and `None`. In this case, +/// `None` should be preferred. +/// * `0b1010.join_into(0b0010)` could be correctly described as both `Identity` and `Element`. In this +/// case, `Identity` should be preferred. /// -/// In general, `AlgebraicStatus` return values are a valid signal for loop termination, but should not be -/// strictly relied upon for other kinds of branching. For example, `Element` might be returned by -/// [ZipperWriting::join](crate::zipper::ZipperWriting::join) instead of `Identity` if the internal representation was changed by the method, -/// however the next call to `join` ought to return `Identity` if nothing new is added. -/// -/// This type mirrors [AlgebraicResult] -#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// This type is a conceptual mirror of [AlgebraicResult] where the result location is unambiguous. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum AlgebraicStatus { - /// A result indicating `self` contains the operation's output - #[default] + /// `self` contains the operation's output, without a stronger guarantee about how it relates to + /// its previous value. + /// + /// This status does not guarantee that the mathematical result differs from the previous value. + /// For example, an operation may return `Element` instead of `Identity` when it changes the internal + /// representation while preserving the same value. Element, - /// A result indicating `self` was unmodified by the operation + /// Indicates that `self` was unmodified by the operation. + /// + /// If `self` was already empty and remains empty, both `Identity` and `None` describe the result. + /// An implementation may return `None` to report the additional fact that no output remains. Identity, - /// A result indicating `self` was completely annhilated and is now empty + /// No output element exists after the operation; `self` is now structurally empty. + /// + /// If `self` was already empty, `Identity` may also describe the result. [Lattice::join_into] + /// must not return `None`, because it joins supplied elements. A higher-level structural join may + /// return `None` when no operand exists at that location, without invoking the value-level join. None, } @@ -545,12 +560,20 @@ pub trait Lattice { /// Implements the union operation between two instances of a type in a partial lattice, resulting in /// the creation of a new result instance + /// + /// A join must be an upper bound of both inputs. Because both arguments are existing elements, + /// returning [AlgebraicResult::None] from this method is a breach of the `Lattice` contract. In + /// particular, an element that represents the lattice bottom is still an element; it must be returned + /// as an `Element` or `Identity`, not confused with the absence of an output element. fn pjoin(&self, other: &Self) -> AlgebraicResult where Self: Sized; /// Implements the union operation between two instances of a type, consuming the `other` input operand, /// and modifying `self` to become the joined type + /// + /// Returning [AlgebraicStatus::None] is a breach of the `Lattice` contract; see [Lattice::pjoin]. fn join_into(&mut self, other: Self) -> AlgebraicStatus where Self: Sized { let result = self.pjoin(&other); + debug_assert!(!result.is_none(), "Lattice::pjoin returned None for a join"); //NOTE: pedantically, the `default_f` ought to assign the `&mut s` to `Self::bottom()`, however there is // no way for a join to get to an empty result except by starting with an empty result, so leaving the // arg alone is functionally the same. @@ -660,6 +683,7 @@ pub(crate) trait HeteroLattice { fn pjoin(&self, other: &OtherT) -> AlgebraicResult where Self: Sized; fn join_into(&mut self, other: OtherT) -> AlgebraicStatus where Self: Sized { let result = self.pjoin(&other); + debug_assert!(!result.is_none(), "pjoin returned None for a join"); //NOTE: See comment on [Lattice::join_into] default impl, regarding using `Self::bottom` for `default_f` in_place_default_impl(result, self, other, |_s| {}, |e| Self::convert(e)) } @@ -690,19 +714,23 @@ impl Lattice for Option { fn pjoin(&self, other: &Option) -> AlgebraicResult { match self { None => match other { - None => { AlgebraicResult::None } + None => { AlgebraicResult::Identity(SELF_IDENT | COUNTER_IDENT) } Some(_) => { AlgebraicResult::Identity(COUNTER_IDENT) } }, Some(l) => match other { None => { AlgebraicResult::Identity(SELF_IDENT) } - Some(r) => { l.pjoin(r).map(|result| Some(result)) } + Some(r) => { + let result = l.pjoin(r); + debug_assert!(!result.is_none(), "Lattice::pjoin returned None for a join"); + result.map(|result| Some(result)) + } } } } fn join_into(&mut self, other: Self) -> AlgebraicStatus { match self { None => { match other { - None => AlgebraicStatus::None, + None => AlgebraicStatus::Identity, Some(r) => { *self = Some(r); AlgebraicStatus::Element @@ -711,7 +739,9 @@ impl Lattice for Option { Some(l) => match other { None => AlgebraicStatus::Identity, Some(r) => { - l.join_into(r) + let status = l.join_into(r); + debug_assert!(!status.is_none(), "Lattice::join_into returned None for a join"); + status } } } @@ -743,6 +773,15 @@ impl DistributiveLattice for Option { } } +#[test] +fn option_join_test() { + assert_eq!(None::<()>.pjoin(&None), AlgebraicResult::Identity(SELF_IDENT | COUNTER_IDENT)); + + let mut value = None::<()>; + assert_eq!(value.join_into(None), AlgebraicStatus::Identity); + assert_eq!(value, None); +} + #[test] fn option_subtract_test() { assert_eq!(Some(()).psubtract(&Some(())), AlgebraicResult::None); diff --git a/src/write_zipper.rs b/src/write_zipper.rs index 9af36476..78550e88 100644 --- a/src/write_zipper.rs +++ b/src/write_zipper.rs @@ -4733,6 +4733,22 @@ mod tests { #[test] fn write_zipper_join_results_test1() { + // Joining two nonexistent foci should leave the destination nonexistent and return `None` + let mut dst_map = PathMap::::new(); + let src_map = PathMap::::new(); + { + let mut wz = dst_map.write_zipper(); + let mut rz = src_map.read_zipper(); + wz.descend_to(b"dst:"); + rz.descend_to(b"src:"); + assert!(!wz.path_exists()); + assert!(!rz.path_exists()); + assert_eq!(wz.join_into(&rz), AlgebraicStatus::None); + assert!(!wz.path_exists()); + } + assert!(dst_map.is_empty()); + assert!(src_map.is_empty()); + let mut map = PathMap::::new(); let head = map.zipper_head(); @@ -6195,6 +6211,11 @@ mod tests { assert!(!rz.is_val(), "dangling path {path:?} unexpectedly has a value"); assert_eq!(rz.child_count(), 0, "dangling path {path:?} unexpectedly has children"); } + fn make_dangling_path_map(paths: &[&[u8]]) -> PathMap<()> { + let mut m = PathMap::<()>::new(); + for path in paths { assert!(m.create_path(path)); } + m + } /// Joining into a dense node whose child at that byte is a dangling sentinel #[test] @@ -6379,6 +6400,97 @@ mod tests { assert!(m.write_zipper().join_k_path_into(2, false)); assert_eq!(keys(&m), vec![b"a".to_vec(), b"ab".to_vec(), b"ac".to_vec()]); assert_eq!(m.val_count(), 3); + assert_valid_trie(m.root()); + } + + /// Head-dropping is a join of the surviving subtries, so it must preserve dangling paths even + /// when they collide. This deliberately uses no values, keeping the question independent of + /// any particular value lattice's treatment of its bottom element. + #[test] + fn write_zipper_join_k_path_dangling_paths() { + //Both paths shorten to "d", so their join is one dangling path at "d". + let mut m = make_dangling_path_map(&[b"abcd", b"dddd"]); + assert!(m.write_zipper().join_k_path_into(3, false)); + assert_eq!(m.read_zipper().child_count(), 1); + assert_dangling_path(&m, b"d"); + assert_valid_trie(m.root()); + + //Dropping the complete paths leaves nothing downstream of the root, so the operation reports + //false. The root focus itself still exists, but it has neither a value nor any children. + let mut m = make_dangling_path_map(&[b"abcd", b"dddd"]); + let status = m.write_zipper().join_k_path_into(4, false); + assert!(!status); + assert!(m.is_empty()); + let rz = m.read_zipper(); + assert!(rz.path_exists()); + assert!(!rz.is_val()); + assert_eq!(rz.child_count(), 0); + assert_valid_trie(m.root()); + + //Distinct suffixes must both survive the join. + let mut m = make_dangling_path_map(&[b"abcd", b"efgh"]); + assert!(m.write_zipper().join_k_path_into(3, false)); + assert_eq!(m.read_zipper().child_count(), 2); + assert_dangling_path(&m, b"d"); + assert_dangling_path(&m, b"h"); + assert_valid_trie(m.root()); + } + + /// Colliding values must honor every possible `Lattice::pjoin` outcome, rather than merely + /// deduplicating the unit values used by the basic issue #84 reproducer. + #[test] + fn write_zipper_join_k_path_colliding_values_follow_lattice() { + //A newly-created lattice element replaces both physical value slots. + for prune in [false, true] { + let mut m = PathMap::::new(); + m.set_val_at(b"aaaa", LatticeProbe(ProbeState::Original)); + m.set_val_at(b"acaa", LatticeProbe(ProbeState::Original)); + assert!(m.write_zipper().join_k_path_into(3, prune)); + assert_eq!(m.iter().map(|(k, _)| k).collect::>>(), vec![b"a".to_vec()]); + assert_eq!(m.val_at(b"a"), Some(&LatticeProbe(ProbeState::Joined))); + assert_eq!(m.val_count(), 1); + assert_valid_trie(m.root()); + } + + //A counter-identity result must retain the right-hand value, not whichever slot is easiest to keep. + let mut m = PathMap::::new(); + m.set_val_at(b"aaaa", false); + m.set_val_at(b"acaa", true); + assert!(m.write_zipper().join_k_path_into(3, false)); + assert_eq!(m.val_at(b"a"), Some(&true)); + assert_eq!(m.val_count(), 1); + assert_valid_trie(m.root()); + + //`Option::None` is a present lattice-bottom value, not the absence of a value from the trie. + let mut m = PathMap::>::new(); + m.set_val_at(b"aaaa", None); + m.set_val_at(b"acaa", None); + assert!(m.write_zipper().join_k_path_into(3, false)); + assert_eq!(m.iter().map(|(k, _)| k).collect::>>(), vec![b"a".to_vec()]); + assert_eq!(m.val_at(b"a"), Some(&None)); + assert_eq!(m.val_count(), 1); + assert_valid_trie(m.root()); + } + + /// Ordinary map and zipper joins use the LineList `merge_guts` path rather than `drop_head_dyn`. + /// A stored `Option::None` remains a present value when equal keys collide there too. + #[test] + fn join_colliding_option_values_retains_present_bottom() { + let mut left = PathMap::>::new(); + left.set_val_at(b"a", None); + let mut right = PathMap::>::new(); + right.set_val_at(b"a", None); + + let joined = left.join(&right); + assert_eq!(joined.val_at(b"a"), Some(&None)); + assert_eq!(joined.val_count(), 1); + assert_valid_trie(joined.root()); + + let status = left.write_zipper().join_into(&right.read_zipper()); + assert_eq!(status, AlgebraicStatus::Identity); + assert_eq!(left.val_at(b"a"), Some(&None)); + assert_eq!(left.val_count(), 1); + assert_valid_trie(left.root()); } /// An emptied root (`remove_branches` at the root always leaves a LineListNode) joined with an empty