   /// returns true for `v**.c3`
   /// returns true for `**`
   /// returns false for `s0.c3` (because e.g. `s1` would not match)
-  public var matchesAllValueFields: Bool {
+  public var topMatchesAnyValueField: Bool {
     switch top.kind {
       case .anyValueFields, .anything: return true
       default: return false
+    }
+  }
+
+  /// Returns true if the path does not have any class projections.
+  /// For example:
+  /// returns true for `v**`
+  /// returns false for `c0`
+  /// returns false for `**` (because '**' can have any number of class projections)
+  public var hasNoClassProjection: Bool {
+    return matches(pattern: Self(.anyValueFields))
+  }
+
+  /// Returns true if the path has at least one class projection.
+  /// For example:
+  /// returns false for `v**`
+  /// returns true for `v**.c0.s1.v**`
+  /// returns false for `**` (because '**' can have zero class projections)
+  public var hasClassProjection: Bool {
+    var p = self
+    while true {
+      let (k, _, numBits) = p.top
+      if k == .root { return false }
+      if k.isClassField { return true }
+      p = p.pop(numBits: numBits)
     }
   }
 

  /// 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 pass 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 |

    assert(kind != .anything || bytes == 0, "'anything' only allowed in last path component")
    var idx = index
    var b = bytes
    if (b >> 56) != 0 {
      // Overflow
      return Self(.anything)
    }
    b = (b << 8) | UInt64(((idx & 0xf) << 4) | (kind.rawValue << 1))
    idx >>= 4
    while idx != 0 {
