       p = p.pop(numBits: numBits)
     }
   }
-  
+
+  /// Pops the last class projection and all following value fields from the tail of the path.
+  /// For example:
+  ///    `s0.e2.3.c4.s1` -> `s0.e2.3`
+  ///    `v**.c1.c4.s1`  -> `v**.c1`
+  ///    `c1.**`         -> `c1.**` (because it's unknown how many class projections are in `**`)
+  public func popLastClassAndValuesFromTail() -> SmallProjectionPath {
+    var p = self
+    var totalBits = 0
+    var neededBits = 0
+    while true {
+      let (k, _, numBits) = p.top
+      if k == .root { break }
+      if k.isClassField {
+        neededBits = totalBits
+        totalBits += numBits
+      } else {
+        totalBits += numBits
+        if !k.isValueField {
+          // k is `anything`
+          neededBits = totalBits
+        }
+      }
+      p = p.pop(numBits: numBits)
+    }
+    if neededBits == 64 { return self }
+    return SmallProjectionPath(bytes: bytes & ((1 << neededBits) - 1))
+  }
+
   /// Returns true if this path matches a pattern path.
   ///
   /// Formally speaking:

    parsing()
    merging()
    matching()
  
    func basicPushPop() {
      let p1 = SmallProjectionPath(.structField, index: 3)

  /// The physical representation of the path. The path components are stored in
  /// reverse order: the first path component is stored in the lowest bits (LSB),
  /// the last component is stored in the highest bits (MSB).
  /// Each path component consists of zero or more "index-overflow" bytes followed
  /// by the "index-kind" main byte (from LSB to MSB).
  ///
  /// index overflow byte:    bit-nr:  | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
