This is an automated email from the ASF dual-hosted git repository.

xiazcy pushed a commit to branch steps-taking-traversal
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit 9ca2f633490fb4efbdcd34ad914475523174deb3
Author: Yang Xia <[email protected]>
AuthorDate: Fri Jun 12 00:14:52 2026 -0700

    Add runtime child-traversal resolution to predicates
    
    Extend P, TextP, NotP, and the connective predicates (AndP/OrP) to carry a
    child traversal whose result is resolved per-traverser at runtime instead 
of a
    literal value. P.resolve() splits the traverser, seeds and runs the child
    traversal, and installs the result as the comparison value for the current 
test
    cycle.
    
    Semantics:
    - Scalar predicates (eq/neq/gt/lt/gte/lte) take the first result; an empty
      result cannot be satisfied and is flagged resolved-empty so steps 
short-circuit.
    - Collection predicates (within/without) resolve to a collection; an empty
      result resolves to an empty set so within(empty) is false and 
without(empty)
      is true, matching literal P.within([])/P.without([]) semantics.
    - Multi-traversal within/without combine the first result of each child.
    - AndP short-circuits resolution at the first child that resolves empty.
    - NotP exposes getWrapped() so traversal collection does not rely on 
negate().
    
    GremlinLang serializes traversal-bearing predicates as op(traversal).
---
 .../gremlin/process/traversal/GremlinLang.java     |  19 +-
 .../tinkerpop/gremlin/process/traversal/NotP.java  |  28 +-
 .../tinkerpop/gremlin/process/traversal/P.java     | 425 +++++++++++++++-
 .../tinkerpop/gremlin/process/traversal/TextP.java |  79 ++-
 .../gremlin/process/traversal/util/AndP.java       |  29 ++
 .../process/traversal/util/ConnectiveP.java        |  18 +
 .../gremlin/process/traversal/util/OrP.java        |  13 +
 .../GremlinLangTraversalRoundTripTest.java         | 120 +++++
 .../tinkerpop/gremlin/process/traversal/PTest.java |  10 +-
 .../gremlin/process/traversal/PTraversalTest.java  | 535 +++++++++++++++++++++
 10 files changed, 1261 insertions(+), 15 deletions(-)

diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
index 3bd1cb5287..289e8a6974 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLang.java
@@ -317,7 +317,11 @@ public class GremlinLang implements Cloneable, 
Serializable {
         final StringBuilder sb = new StringBuilder();
         if (p instanceof TextP) {
             sb.append("TextP.").append(p.getPredicateName()).append("(");
-            sb.append(argAsString(p.getValue()));
+            if (p.hasTraversal()) {
+                sb.append(argAsString(p.getTraversalValue()));
+            } else {
+                sb.append(argAsString(p.getValue()));
+            }
         } else if (p instanceof ConnectiveP) {
             // ConnectiveP gets some special handling because it's reduced to 
and(P, P, P) and we want it
             // generated the way it was written which was P.and(P).and(P)
@@ -337,6 +341,19 @@ public class GremlinLang implements Cloneable, 
Serializable {
         } else if (p instanceof NotP) {
             sb.append("P.not(");
             sb.append(argAsString(p.negate())); // Wrap internal P in 
`P.not(%s)`
+        } else if (p.hasTraversal()) {
+            // Traversal-bearing predicate: serialize as 
P.op(traversalGremlinLang)
+            sb.append("P.").append(p.getPredicateName()).append("(");
+            if (p.getTraversalValues() != null) {
+                // Multi-traversal predicate (within/without with multiple 
traversals)
+                final List<Traversal.Admin<?, ?>> traversals = 
p.getTraversalValues();
+                for (int i = 0; i < traversals.size(); i++) {
+                    if (i > 0) sb.append(",");
+                    sb.append(argAsString(traversals.get(i)));
+                }
+            } else {
+                sb.append(argAsString(p.getTraversalValue()));
+            }
         } else {
             sb.append("P.").append(p.getPredicateName()).append("(");
             sb.append(argAsString(p.getValue()));
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java
index f77f0f5974..aca1d241e4 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/NotP.java
@@ -59,17 +59,41 @@ public class NotP<V> extends P<V> {
     }
 
     /**
-     * Returns the original unwrapped P contained within this NotP, as double 
negation cancels out.
+     * Returns the inner predicate wrapped by this NotP.
+     */
+    public P<V> getWrapped() {
+        return this.originalP;
+    }
+
+    /**
+     * Returns the original unwrapped P, since double negation cancels out.
+     * @apiNote Functionally identical to {@link #getWrapped()}, but fulfills 
the {@link java.util.function.Predicate#negate()} contract.
      */
     @Override
     public P<V> negate() {
-        return originalP;
+        return getWrapped();
     }
 
     public P<V> clone() {
         return new NotP<>(this.originalP.clone());
     }
 
+    @Override
+    public boolean hasTraversal() {
+        return super.hasTraversal() || this.originalP.hasTraversal();
+    }
+
+    @Override
+    public boolean isResolvedEmpty() {
+        return this.originalP.isResolvedEmpty();
+    }
+
+    @Override
+    public void resolve(final Traverser.Admin<?> traverser) {
+        super.resolve(traverser);
+        this.originalP.resolve(traverser);
+    }
+
     /**
      * A NotPBiPredicate wraps a PBiPredicate and represents its negation.
      */
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
index afc41b5681..5f0606bd07 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/P.java
@@ -20,9 +20,13 @@ package org.apache.tinkerpop.gremlin.process.traversal;
 
 import org.apache.commons.collections4.CollectionUtils;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
+import org.apache.tinkerpop.gremlin.process.traversal.step.TraversalParent;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.BulkSet;
 import org.apache.tinkerpop.gremlin.process.traversal.util.AndP;
+import org.apache.tinkerpop.gremlin.process.traversal.util.ConnectiveP;
+import org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
 import org.apache.tinkerpop.gremlin.process.traversal.util.OrP;
+import 
org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator;
 
 import java.io.Serializable;
 import java.util.ArrayList;
@@ -31,7 +35,10 @@ import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Optional;
 import java.util.Set;
 import java.util.function.Predicate;
@@ -49,10 +56,23 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
     protected Map<String, V> variables = new HashMap<>();
     protected Collection<V> literals = Collections.EMPTY_LIST;
     private boolean isCollection = false;
+    private boolean resolvedEmpty = false;
+    private Traversal.Admin<?, ?> traversalValue;
+    private List<Traversal.Admin<?, ?>> traversalValues;
 
     public P(final PBiPredicate<V, V> biPredicate, final V value) {
-        setValue(value);
         this.biPredicate = biPredicate;
+        // If the value is a DefaultGraphTraversal (the type created by 
__.xxx() anonymous traversals),
+        // treat it as a child traversal rather than a literal. This handles 
the case where Java's
+        // overload resolution picks P(BiPredicate, V) instead of 
P(BiPredicate, Traversal) when
+        // the caller passes a GraphTraversal. We specifically check for 
DefaultGraphTraversal
+        // rather than Traversal to avoid catching internal traversal types 
like ConstantTraversal,
+        // ValueTraversal, and IdentityTraversal which are used as literal 
values in P.
+        if (value instanceof 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal) 
{
+            this.traversalValue = ((Traversal<?, ?>) value).asAdmin();
+        } else {
+            setValue(value);
+        }
     }
 
     public P(final PBiPredicate<V, V> biPredicate, final GValue<V> value) {
@@ -81,6 +101,29 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
         this.isCollection = isCollection;
     }
 
+    /**
+     * Constructs a {@code P} with a child traversal whose result is resolved 
at runtime against the current
+     * traverser. The literals and variables are left at their defaults and 
will be populated when
+     * {@link #resolve(Traverser.Admin)} is called.
+     */
+    public P(final PBiPredicate<V, V> biPredicate, final Traversal.Admin<?, ?> 
traversalValue) {
+        this.biPredicate = biPredicate;
+        this.traversalValue = traversalValue;
+    }
+
+    /**
+     * Constructs a {@code P} with multiple child traversals whose results are 
unioned at runtime against the
+     * current traverser. Only valid for collection predicates ({@link 
Contains#within}, {@link Contains#without}).
+     * The literals and variables are left at their defaults and will be 
populated when
+     * {@link #resolve(Traverser.Admin)} is called.
+     *
+     * @since 4.0.0
+     */
+    public P(final PBiPredicate<V, V> biPredicate, final 
List<Traversal.Admin<?, ?>> traversalValues) {
+        this.biPredicate = biPredicate;
+        this.traversalValues = traversalValues;
+    }
+
     public PBiPredicate<V, V> getBiPredicate() {
         return this.biPredicate;
     }
@@ -162,6 +205,10 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
             result ^= this.variables.hashCode();
         if (null != this.literals)
             result ^= this.literals.hashCode();
+        if (null != this.traversalValue)
+            result ^= this.traversalValue.hashCode();
+        if (null != this.traversalValues)
+            result ^= this.traversalValues.hashCode();
         return result;
     }
 
@@ -171,7 +218,9 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
                 ((P) other).getClass().equals(this.getClass()) &&
                 ((P) other).getBiPredicate().equals(this.biPredicate) &&
                 ((((P) other).variables == null && this.variables == null) || 
(((P) other).variables != null && ((P) 
other).variables.equals(this.variables))) &&
-                ((((P) other).literals == null && this.literals == null) || 
(((P) other).literals != null && CollectionUtils.isEqualCollection(((P) 
other).literals, this.literals)));
+                ((((P) other).literals == null && this.literals == null) || 
(((P) other).literals != null && CollectionUtils.isEqualCollection(((P) 
other).literals, this.literals))) &&
+                Objects.equals(((P) other).traversalValue, 
this.traversalValue) &&
+                Objects.equals(((P) other).traversalValues, 
this.traversalValues);
     }
 
     @Override
@@ -200,7 +249,17 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
 
     public P<V> clone() {
         try {
-            return (P<V>) super.clone();
+            final P<V> clone = (P<V>) super.clone();
+            if (this.traversalValue != null) {
+                clone.traversalValue = this.traversalValue.clone();
+            }
+            if (this.traversalValues != null) {
+                clone.traversalValues = new 
ArrayList<>(this.traversalValues.size());
+                for (final Traversal.Admin<?, ?> tv : this.traversalValues) {
+                    clone.traversalValues.add(tv.clone());
+                }
+            }
+            return clone;
         } catch (final CloneNotSupportedException e) {
             throw new IllegalStateException(e.getMessage(), e);
         }
@@ -224,6 +283,187 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
         return results;
     }
 
+    /**
+     * Determines if this predicate holds a child traversal whose result is 
resolved at runtime.
+     */
+    public boolean hasTraversal() {
+        return this.traversalValue != null || (this.traversalValues != null && 
!this.traversalValues.isEmpty());
+    }
+
+    /**
+     * Returns {@code true} if the most recent call to {@link 
#resolve(Traverser.Admin)} produced no results.
+     * Steps should check this after calling {@code resolve()} and 
short-circuit appropriately rather than
+     * calling {@link #test(Object)}, which would compare against {@code null}.
+     */
+    public boolean isResolvedEmpty() {
+        return this.resolvedEmpty;
+    }
+
+    /**
+     * Gets the child traversal value, if one was provided. Returns {@code 
null} when this predicate uses
+     * literal values or variables.
+     */
+    public Traversal.Admin<?, ?> getTraversalValue() {
+        return this.traversalValue;
+    }
+
+    /**
+     * Gets the list of child traversal values for multi-traversal predicates 
(e.g., {@code within(trav1, trav2)}).
+     * Returns {@code null} when this predicate uses a single traversal or 
literal values.
+     *
+     * @since 4.0.0
+     */
+    public List<Traversal.Admin<?, ?>> getTraversalValues() {
+        return this.traversalValues;
+    }
+
+    /**
+     * Resolves the child traversal against the given traverser, replacing the 
traversal value with the
+     * resolved literal(s) for this test cycle. If no traversal is present, 
this method returns immediately.
+     *
+     * <p>For all predicates, only the first result from the child traversal 
is used,
+     * consistent with {@code by(traversal)} semantics. For collection 
predicates
+     * ({@link Contains#within}, {@link Contains#without}), the first result 
should be a
+     * {@link Collection} (e.g., produced by {@code fold()}).</p>
+     *
+     * <p>When multiple traversals are present (via {@link #traversalValues}), 
each traversal is evaluated
+     * independently, the first result from each is taken, and results are 
combined into a single collection.
+     * This is only valid for collection predicates ({@link Contains#within}, 
{@link Contains#without}).</p>
+     */
+    @SuppressWarnings("unchecked")
+    public void resolve(final Traverser.Admin<?> traverser) {
+        if (this.traversalValues != null && !this.traversalValues.isEmpty()) {
+            resolveMultipleTraversals(traverser);
+            return;
+        }
+
+        if (this.traversalValue == null) return;
+
+        final Traversal.Admin<Object, Object> trav = (Traversal.Admin<Object, 
Object>) (Traversal.Admin) this.traversalValue;
+        prepareChildTraversal(traverser, trav);
+
+        try {
+            if (!trav.hasNext()) {
+                // No results from the child traversal. For collection 
predicates (within/without) this is a
+                // legitimate empty set: within(empty) -> false, 
without(empty) -> true. Resolve to an empty
+                // collection and let Contains.test() apply the correct 
semantics rather than short-circuiting.
+                // For scalar predicates (eq/gt/lt/etc.) there is no 
comparison value, so flag as resolved-empty
+                // and let the step short-circuit (cannot satisfy).
+                this.literals = Collections.emptyList();
+                if (this.biPredicate instanceof Contains) {
+                    this.resolvedEmpty = false;
+                    this.isCollection = true;
+                } else {
+                    this.resolvedEmpty = true;
+                    this.isCollection = false;
+                }
+            } else {
+                this.resolvedEmpty = false;
+                final Object firstResult = trav.next();
+                if (this.biPredicate instanceof Contains) {
+                    if (firstResult instanceof Collection) {
+                        this.literals = (Collection<V>) firstResult;
+                    } else {
+                        this.literals = Collections.singletonList((V) 
firstResult);
+                    }
+                    this.isCollection = true;
+                } else {
+                    setValue((V) firstResult);
+                }
+            }
+        } finally {
+            CloseableIterator.closeIterator(trav);
+        }
+    }
+
+    /**
+     * Resolves multiple child traversals, taking the first result from each 
and combining into a collection.
+     * Only valid for collection predicates ({@link Contains}).
+     */
+    @SuppressWarnings("unchecked")
+    private void resolveMultipleTraversals(final Traverser.Admin<?> traverser) 
{
+        final List<Object> allResults = new ArrayList<>();
+
+        for (final Traversal.Admin<?, ?> tv : this.traversalValues) {
+            final Traversal.Admin<Object, Object> trav = 
(Traversal.Admin<Object, Object>) (Traversal.Admin) tv;
+            prepareChildTraversal(traverser, trav);
+
+            try {
+                if (trav.hasNext()) {
+                    final Object firstResult = trav.next();
+                    if (firstResult instanceof Collection) {
+                        allResults.addAll((Collection<?>) firstResult);
+                    } else {
+                        allResults.add(firstResult);
+                    }
+                }
+            } finally {
+                CloseableIterator.closeIterator(trav);
+            }
+        }
+
+        // Multi-traversal resolution is only valid for collection predicates 
(within/without). An empty
+        // combined result is a legitimate empty set, so resolve to an empty 
collection and let Contains.test()
+        // apply the correct semantics (within(empty) -> false, without(empty) 
-> true) instead of short-circuiting.
+        this.resolvedEmpty = false;
+        this.isCollection = true;
+        this.literals = allResults.isEmpty()
+                ? Collections.emptyList()
+                : (Collection<V>) (Collection<?>) allResults;
+    }
+
+    /**
+     * Prepares a child traversal for evaluation by splitting the current 
traverser and seeding it.
+     */
+    @SuppressWarnings("unchecked")
+    private static void prepareChildTraversal(final Traverser.Admin<?> 
traverser, final Traversal.Admin<Object, Object> trav) {
+        final Traverser.Admin<Object> split = (Traverser.Admin<Object>) 
traverser.split();
+        split.setSideEffects(trav.getSideEffects());
+        split.setBulk(1L);
+        trav.reset();
+        trav.addStart(split);
+    }
+
+    //////////////// predicate traversal utilities
+
+    /**
+     * Recursively integrates all child traversals found in the predicate tree 
into the given parent step.
+     * Handles {@link ConnectiveP} (recurses into children) and {@link NotP} 
(recurses into wrapped predicate).
+     */
+    public static void integrateTraversals(final P<?> p, final TraversalParent 
parent) {
+        if (p instanceof ConnectiveP) {
+            for (final P<?> child : ((ConnectiveP<?>) p).getPredicates()) {
+                integrateTraversals(child, parent);
+            }
+        } else if (p instanceof NotP) {
+            integrateTraversals(((NotP<?>) p).getWrapped(), parent);
+        } else if (p.getTraversalValue() != null) {
+            parent.integrateChild(p.getTraversalValue());
+        } else if (p.getTraversalValues() != null) {
+            for (final Traversal.Admin<?, ?> tv : p.getTraversalValues()) {
+                parent.integrateChild(tv);
+            }
+        }
+    }
+
+    /**
+     * Recursively collects all child traversals from a predicate tree.
+     * Handles {@link ConnectiveP} (recurses into children) and {@link NotP} 
(recurses into wrapped predicate).
+     */
+    public static void collectTraversals(final P<?> p, final 
List<Traversal.Admin<?, ?>> traversals) {
+        if (p instanceof ConnectiveP) {
+            for (final P<?> child : ((ConnectiveP<?>) p).getPredicates()) {
+                collectTraversals(child, traversals);
+            }
+        } else if (p instanceof NotP) {
+            collectTraversals(((NotP<?>) p).getWrapped(), traversals);
+        } else if (p.getTraversalValue() != null) {
+            traversals.add(p.getTraversalValue());
+        } else if (p.getTraversalValues() != null) {
+            traversals.addAll(p.getTraversalValues());
+        }
+    }
+
     //////////////// statics
 
     /**
@@ -394,6 +634,8 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
      * @since 3.0.0-incubating
      */
     public static <V> P<V> within(final V... values) {
+        final P<V> traversalP = handleContainsVarargs(values, Contains.within, 
"within");
+        if (traversalP != null) return traversalP;
         final V[] v = null == values ? (V[]) new Object[] { null } : (V[]) 
values;
         return P.within(Arrays.asList(v));
     }
@@ -427,6 +669,8 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
      * @since 3.0.0-incubating
      */
     public static <V> P<V> without(final V... values) {
+        final P<V> traversalP = handleContainsVarargs(values, 
Contains.without, "without");
+        if (traversalP != null) return traversalP;
         final V[] v = null == values ? (V[]) new Object[] { null } : values;
         return P.without(Arrays.asList(v));
     }
@@ -453,6 +697,106 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
         return new P(Contains.without, value);
     }
 
+    /**
+     * Determines if values are equal using a child traversal resolved at 
runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> eq(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.eq, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if values are not equal using a child traversal resolved at 
runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> neq(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.neq, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is less than another using a child traversal 
resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> lt(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.lt, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is less than or equal to another using a child 
traversal resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> lte(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.lte, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is greater than another using a child traversal 
resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> gt(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.gt, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is greater than or equal to another using a child 
traversal resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> gte(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Compare.gte, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is within the results of a child traversal 
resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> within(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Contains.within, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is within the union of results from multiple 
child traversals resolved at runtime.
+     * Each traversal is evaluated independently and results are combined into 
a single collection.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> within(final Traversal<?, ?> first, final 
Traversal<?, ?> second, final Traversal<?, ?>... more) {
+        return containsTraversals(Contains.within, first, second, more);
+    }
+
+    /**
+     * Determines if a value is not within the results of a child traversal 
resolved at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> without(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new P(Contains.without, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if a value is not within the union of results from multiple 
child traversals resolved at runtime.
+     * Each traversal is evaluated independently and results are combined into 
a single collection.
+     *
+     * @since 4.0.0
+     */
+    public static <V> P<V> without(final Traversal<?, ?> first, final 
Traversal<?, ?> second, final Traversal<?, ?>... more) {
+        return containsTraversals(Contains.without, first, second, more);
+    }
+
     /**
      * Determines if a value is of a type denoted by {@code GType}.
      *
@@ -497,4 +841,79 @@ public class P<V> implements Predicate<V>, Serializable, 
Cloneable {
     public static <V> P<V> not(final P<V> predicate) {
         return predicate.negate();
     }
+
+    /**
+     * Handles varargs traversal detection for within/without. Returns a P if 
traversals were found,
+     * or null if the values are plain literals.
+     */
+    @SuppressWarnings("unchecked")
+    private static <V> P<V> handleContainsVarargs(final V[] values, final 
PBiPredicate predicate, final String stepName) {
+        if (values != null && values.length == 1 && values[0] instanceof 
Traversal) {
+            final Traversal<?, ?> trav = (Traversal<?, ?>) values[0];
+            ChildTraversalValidator.validate(trav.asAdmin());
+            return new P(predicate, trav.asAdmin());
+        }
+        if (values != null && values.length > 1 && allTraversals(values)) {
+            final List<Traversal.Admin<?, ?>> traversals = new 
ArrayList<>(values.length);
+            for (final V v : values) {
+                traversals.add(((Traversal<?, ?>) v).asAdmin());
+            }
+            for (final Traversal.Admin<?, ?> tv : traversals) {
+                ChildTraversalValidator.validate(tv);
+            }
+            return new P(predicate, traversals);
+        }
+        if (values != null && values.length > 1 && anyTraversals(values)) {
+            throw new IllegalArgumentException(
+                    "Cannot mix traversals and literal values in " + stepName 
+ "(). " +
+                    "Use " + stepName + "(__.constant(val1), 
__.constant(val2)) to wrap all values as traversals.");
+        }
+        return null;
+    }
+
+    /**
+     * Creates a Contains predicate from multiple validated child traversals.
+     */
+    @SuppressWarnings("unchecked")
+    private static <V> P<V> containsTraversals(final PBiPredicate predicate, 
final Traversal<?, ?> first,
+                                               final Traversal<?, ?> second, 
final Traversal<?, ?>... more) {
+        final List<Traversal.Admin<?, ?>> traversals = new ArrayList<>(2 + 
(more != null ? more.length : 0));
+        traversals.add(first.asAdmin());
+        traversals.add(second.asAdmin());
+        if (more != null) {
+            for (final Traversal<?, ?> tv : more) {
+                traversals.add(tv.asAdmin());
+            }
+        }
+        for (final Traversal.Admin<?, ?> tv : traversals) {
+            ChildTraversalValidator.validate(tv);
+        }
+        return new P(predicate, traversals);
+    }
+
+    /**
+     * Checks if all elements in the array are {@link Traversal} instances 
(specifically
+     * {@link 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal}).
+     */
+    private static <V> boolean allTraversals(final V[] values) {
+        for (final V v : values) {
+            if (!(v instanceof 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal))
 {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Checks if any element in the array is a {@link Traversal} instance 
(specifically
+     * {@link 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal}).
+     */
+    private static <V> boolean anyTraversals(final V[] values) {
+        for (final V v : values) {
+            if (v instanceof 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.DefaultGraphTraversal) 
{
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
index d1d8c21019..c0bd155185 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/TextP.java
@@ -19,6 +19,7 @@
 package org.apache.tinkerpop.gremlin.process.traversal;
 
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
+import 
org.apache.tinkerpop.gremlin.process.traversal.util.ChildTraversalValidator;
 
 import java.util.Collection;
 import java.util.Map;
@@ -38,6 +39,16 @@ public class TextP extends P<String> {
         super(biPredicate, value);
     }
 
+    /**
+     * Constructs a {@code TextP} with a child traversal whose result is 
resolved at runtime against the
+     * current traverser. The traversal must produce a {@code String} result.
+     *
+     * @since 4.0.0
+     */
+    public TextP(final PBiPredicate<String, String> biPredicate, final 
Traversal.Admin<?, ?> traversalValue) {
+        super(biPredicate, traversalValue);
+    }
+
     protected TextP(final PBiPredicate<String, String> biPredicate, final 
Collection<String> literals, final Map<String, String> variables) {
         super(biPredicate, literals, variables, false);
     }
@@ -76,6 +87,16 @@ public class TextP extends P<String> {
         return new TextP(Text.startingWith, value);
     }
 
+    /**
+     * Determines if String does start with the value resolved from a child 
traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP startingWith(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.startingWith, traversalValue.asAdmin());
+    }
+
     /**
      * Determines if String does not start with the given value.
      *
@@ -95,7 +116,17 @@ public class TextP extends P<String> {
     }
 
     /**
-     * Determines if String does start with the given value.
+     * Determines if String does not start with the value resolved from a 
child traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP notStartingWith(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.notStartingWith, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if String does end with the given value.
      *
      * @since 3.4.0
      */
@@ -104,7 +135,7 @@ public class TextP extends P<String> {
     }
 
     /**
-     * Determines if String does start with the given value.
+     * Determines if String does end with the given value.
      *
      * @since 3.8.0
      */
@@ -113,7 +144,17 @@ public class TextP extends P<String> {
     }
 
     /**
-     * Determines if String does not start with the given value.
+     * Determines if String does end with the value resolved from a child 
traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP endingWith(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.endingWith, traversalValue.asAdmin());
+    }
+
+    /**
+     * Determines if String does not end with the given value.
      *
      * @since 3.4.0
      */
@@ -122,7 +163,7 @@ public class TextP extends P<String> {
     }
 
     /**
-     * Determines if String does not start with the given value.
+     * Determines if String does not end with the given value.
      *
      * @since 3.8.0
      */
@@ -130,6 +171,16 @@ public class TextP extends P<String> {
         return new TextP(Text.notEndingWith, value);
     }
 
+    /**
+     * Determines if String does not end with the value resolved from a child 
traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP notEndingWith(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.notEndingWith, traversalValue.asAdmin());
+    }
+
     /**
      * Determines if String does contain the given value.
      *
@@ -148,6 +199,16 @@ public class TextP extends P<String> {
         return new TextP(Text.containing, value);
     }
 
+    /**
+     * Determines if String does contain the value resolved from a child 
traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP containing(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.containing, traversalValue.asAdmin());
+    }
+
     /**
      * Determines if String does not contain the given value.
      *
@@ -165,6 +226,16 @@ public class TextP extends P<String> {
     public static TextP notContaining(final GValue<String> value) {
         return new TextP(Text.notContaining, value);
     }
+
+    /**
+     * Determines if String does not contain the value resolved from a child 
traversal at runtime.
+     *
+     * @since 4.0.0
+     */
+    public static TextP notContaining(final Traversal<?, ?> traversalValue) {
+        ChildTraversalValidator.validate(traversalValue.asAdmin());
+        return new TextP(Text.notContaining, traversalValue.asAdmin());
+    }
     
     /**           
      * Determines if String has a match with the given regex pattern. The 
TinkerPop reference implementation uses
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java
index db5cced9ec..1275e08f04 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/AndP.java
@@ -20,6 +20,7 @@ package org.apache.tinkerpop.gremlin.process.traversal.util;
 
 import org.apache.tinkerpop.gremlin.process.traversal.P;
 import org.apache.tinkerpop.gremlin.process.traversal.PBiPredicate;
+import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
 import org.apache.tinkerpop.gremlin.structure.util.StringFactory;
 
 import java.io.Serializable;
@@ -56,6 +57,34 @@ public final class AndP<V> extends ConnectiveP<V> {
         return new OrP<>(this.predicates);
     }
 
+    /**
+     * Resolves child predicates with short-circuiting. Because a conjunction 
fails as soon as any child
+     * predicate cannot be satisfied, resolution stops at the first child that 
resolves empty (i.e. a scalar
+     * predicate whose child traversal produced no comparison value). This 
avoids evaluating the remaining
+     * child traversals, which may be expensive. Collection predicates 
(within/without) never resolve empty
+     * (they resolve to an empty collection), so they do not trigger the 
short-circuit.
+     */
+    @Override
+    public void resolve(final Traverser.Admin<?> traverser) {
+        // No super.resolve(): a connective predicate carries no child 
traversal of its own; only its
+        // operands do. Resolving each operand directly is sufficient (and 
enables the short-circuit below).
+        for (final P<V> p : this.predicates) {
+            if (p.hasTraversal()) {
+                p.resolve(traverser);
+                if (p.isResolvedEmpty()) return;
+            }
+        }
+    }
+
+    @Override
+    public boolean isResolvedEmpty() {
+        // AND short-circuits: if any child resolved empty, the conjunction 
cannot be satisfied
+        for (final P<V> p : this.predicates) {
+            if (p.hasTraversal() && p.isResolvedEmpty()) return true;
+        }
+        return false;
+    }
+
     @Override
     public String toString() {
         return "and(" + StringFactory.removeEndBrackets(this.predicates) + ")";
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java
index ef5a1142c3..fa277d2806 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/ConnectiveP.java
@@ -19,6 +19,7 @@
 package org.apache.tinkerpop.gremlin.process.traversal.util;
 
 import org.apache.tinkerpop.gremlin.process.traversal.P;
+import org.apache.tinkerpop.gremlin.process.traversal.Traverser;
 import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
 
 import java.util.ArrayList;
@@ -134,4 +135,21 @@ public abstract class ConnectiveP<V> extends P<V> {
         }
         return allGValues;
     }
+
+    @Override
+    public boolean hasTraversal() {
+        if (super.hasTraversal()) return true;
+        for (final P<?> p : this.predicates) {
+            if (p.hasTraversal()) return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void resolve(final Traverser.Admin<?> traverser) {
+        super.resolve(traverser);
+        for (final P<?> p : this.predicates) {
+            p.resolve(traverser);
+        }
+    }
 }
\ No newline at end of file
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java
index 2a0c27c7af..0a054033e9 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/util/OrP.java
@@ -57,6 +57,19 @@ public final class OrP<V> extends ConnectiveP<V> {
         return new AndP<>(this.predicates);
     }
 
+    @Override
+    public boolean isResolvedEmpty() {
+        // OR short-circuits: only empty if ALL traversal-bearing children 
resolved empty
+        boolean anyTraversal = false;
+        for (final P<V> p : this.predicates) {
+            if (p.hasTraversal()) {
+                anyTraversal = true;
+                if (!p.isResolvedEmpty()) return false;
+            }
+        }
+        return anyTraversal;
+    }
+
     @Override
     public String toString() {
         return "or(" + StringFactory.removeEndBrackets(this.predicates) + ")";
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java
new file mode 100644
index 0000000000..391938b9ca
--- /dev/null
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/GremlinLangTraversalRoundTripTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal;
+
+import 
org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.GraphTraversalSource;
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.language.grammar.GremlinAntlrToJava;
+import org.apache.tinkerpop.gremlin.language.grammar.GremlinQueryParser;
+import org.apache.tinkerpop.gremlin.structure.util.empty.EmptyGraph;
+import org.junit.Test;
+
+import static 
org.apache.tinkerpop.gremlin.process.traversal.AnonymousTraversalSource.traversal;
+import static org.junit.Assert.assertEquals;
+
+/**
+ * Property 7: GremlinLang serialization round-trip preserves traversal 
arguments.
+ * <p>
+ * For any step containing a child traversal argument, serializing to 
GremlinLang and parsing back
+ * SHALL produce a structurally equivalent traversal.
+ * <p>
+ * <b>Validates: Requirements 1.3, 3.6, 4.5, 6.5, 7.3</b>
+ */
+public class GremlinLangTraversalRoundTripTest {
+
+    private static final GraphTraversalSource g = 
traversal().with(EmptyGraph.instance());
+
+    /**
+     * Serializes a traversal to GremlinLang, parses it back, and verifies 
structural equivalence
+     * by comparing the GremlinLang output of both the original and the 
round-tripped traversal.
+     */
+    private void assertRoundTrip(final Traversal<?, ?> traversal) {
+        final String originalGremlin = 
traversal.asAdmin().getGremlinLang().getGremlin();
+
+        // Parse the GremlinLang string back into a traversal
+        final GremlinAntlrToJava antlr = new GremlinAntlrToJava();
+        final Object parsed = GremlinQueryParser.parse(originalGremlin, antlr);
+
+        // Get the GremlinLang of the parsed traversal
+        final String roundTrippedGremlin = ((Traversal<?, ?>) 
parsed).asAdmin().getGremlinLang().getGremlin();
+
+        assertEquals("GremlinLang round-trip should preserve traversal 
structure for: " + originalGremlin,
+                originalGremlin, roundTrippedGremlin);
+    }
+
+    @Test
+    public void shouldRoundTripHasWithTraversalValue() {
+        // g.V().has("name", __.values("x"))
+        assertRoundTrip(g.V().has("name", __.values("x")));
+    }
+
+    @Test
+    public void shouldRoundTripHasWithPredicateTraversal() {
+        // g.V().has("name", P.eq(__.values("x")))
+        assertRoundTrip(g.V().has("name", P.eq(__.values("x").asAdmin())));
+    }
+
+    @Test
+    public void shouldRoundTripVWithTraversal() {
+        // g.V().V(__.select("ids"))
+        assertRoundTrip(g.V().V(__.select("ids")));
+    }
+
+    @Test
+    public void shouldRoundTripPropertyWithTraversal() {
+        // g.V().property("key", __.select("val"))
+        assertRoundTrip(g.V().property("key", __.select("val")));
+    }
+
+    @Test
+    public void shouldRoundTripHasWithConstantTraversal() {
+        // g.V().has("name", __.constant("marko"))
+        assertRoundTrip(g.V().has("name", __.constant("marko")));
+    }
+
+    @Test
+    public void shouldRoundTripLiteralHasUnchanged() {
+        // Backward compatibility: literal has() should still round-trip 
correctly
+        assertRoundTrip(g.V().has("name", "marko"));
+    }
+
+    @Test
+    public void shouldRoundTripLiteralPredicateUnchanged() {
+        // Backward compatibility: literal P.eq() should still round-trip 
correctly
+        assertRoundTrip(g.V().has("name", P.eq("marko")));
+    }
+
+    @Test
+    public void shouldRoundTripHasWithPGtTraversal() {
+        // g.V().has("age", P.gt(__.constant(30)))
+        assertRoundTrip(g.V().has("age", P.gt(__.constant(30).asAdmin())));
+    }
+
+    @Test
+    public void shouldRoundTripEWithTraversal() {
+        // g.V().E(__.select("edgeIds"))
+        assertRoundTrip(g.V().E(__.select("edgeIds")));
+    }
+
+    @Test
+    public void shouldRoundTripPropertyWithMapTraversal() {
+        // g.V().property(__.V().project("a").by("name")) - the Map-producing 
form
+        assertRoundTrip(g.V().property(__.V().project("a").by("name")));
+    }
+}
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
index 4a8959a1ee..80f65c59db 100644
--- 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTest.java
@@ -65,8 +65,8 @@ public class PTest {
                     {P.eq(-0), +0, true},
                     {P.eq(0), 1, false},
                     {P.eq(0), null, false},
-                    {P.eq(null), null, true},
-                    {P.eq(null), 0, false},
+                    {P.eq((Object) null), null, true},
+                    {P.eq((Object) null), 0, false},
                     {P.eq(Double.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, 
false},
                     {P.eq(Float.POSITIVE_INFINITY), Float.NEGATIVE_INFINITY, 
false},
                     {P.eq(Float.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, 
false},
@@ -82,8 +82,8 @@ public class PTest {
                     {P.neq(-0), +0, false},
                     {P.neq(0), 1, true},
                     {P.neq(0), null, true},
-                    {P.neq(null), null, false},
-                    {P.neq(null), 0, true},
+                    {P.neq((Object) null), null, false},
+                    {P.neq((Object) null), 0, true},
                     {P.neq(Double.POSITIVE_INFINITY), 
Double.NEGATIVE_INFINITY, true},
                     {P.neq(Float.POSITIVE_INFINITY), Float.NEGATIVE_INFINITY, 
true},
                     {P.neq(Float.POSITIVE_INFINITY), Double.NEGATIVE_INFINITY, 
true},
@@ -468,7 +468,7 @@ public class PTest {
             assertTrue(predicate.test(null));
             assertFalse(predicate.test(INITIAL_VALUE));
             assertEquals("eq", predicate.toString());
-            assertEquals(predicate, P.eq(null));
+            assertEquals(predicate, P.eq((Object) null));
             assertNotEquals(predicate, P.eq(INITIAL_VALUE));
         }
 
diff --git 
a/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
new file mode 100644
index 0000000000..d52b45ae49
--- /dev/null
+++ 
b/gremlin-core/src/test/java/org/apache/tinkerpop/gremlin/process/traversal/PTraversalTest.java
@@ -0,0 +1,535 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.tinkerpop.gremlin.process.traversal;
+
+import org.apache.tinkerpop.gremlin.process.traversal.dsl.graph.__;
+import org.apache.tinkerpop.gremlin.process.traversal.step.GValue;
+import org.apache.tinkerpop.gremlin.process.traversal.traverser.B_O_Traverser;
+import org.junit.Test;
+import org.junit.experimental.runners.Enclosed;
+import org.junit.runner.RunWith;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Tests for traversal-aware behavior in {@link P}, covering traversal 
detection accuracy
+ * and single-value predicate rejection of multiple traversal results.
+ */
+@RunWith(Enclosed.class)
+public class PTraversalTest {
+
+    /**
+     * Property 10: Traversal detection in predicates is accurate.
+     * <p>
+     * For any P instance containing a child traversal, {@code 
P.hasTraversal()} SHALL return true.
+     * For any P instance containing only literal values or GValue variables,
+     * {@code P.hasTraversal()} SHALL return false.
+     * <p>
+     * <b>Validates: Requirements 9.4</b>
+     */
+    public static class TraversalDetectionTest {
+
+        @Test
+        public void shouldDetectTraversalInComparisonPredicate() {
+            final P<Object> p = P.eq(__.identity().asAdmin());
+            assertThat(p.hasTraversal(), is(true));
+        }
+
+        @Test
+        public void shouldDetectTraversalInCollectionPredicate() {
+            final P<Object> p = P.within(__.inject(1, 2, 3).asAdmin());
+            assertThat(p.hasTraversal(), is(true));
+        }
+
+        @Test
+        public void shouldNotDetectTraversalInLiteralPredicate() {
+            final P<String> p = P.eq("value");
+            assertThat(p.hasTraversal(), is(false));
+        }
+
+        @Test
+        public void shouldNotDetectTraversalInGValuePredicate() {
+            final P<String> p = P.eq(GValue.of("x", "value"));
+            assertThat(p.hasTraversal(), is(false));
+        }
+
+        @Test
+        public void shouldReturnTraversalValueWhenPresent() {
+            final Traversal.Admin<?, ?> traversal = __.inject(42).asAdmin();
+            final P<Object> p = P.eq(traversal);
+            assertThat(p.getTraversalValue() == traversal, is(true));
+        }
+
+        @Test
+        public void shouldReturnNullTraversalValueForLiteral() {
+            final P<String> p = P.eq("value");
+            assertThat(p.getTraversalValue() == null, is(true));
+        }
+    }
+
+    /**
+     * Property 3: Single-value predicate rejects multiple traversal results.
+     * <p>
+     * For any single-value predicate (eq, neq, gt, lt, gte, lte) and any 
child traversal that produces
+     * more than one result, the predicate SHALL throw an 
IllegalArgumentException.
+     * For any collection predicate (within, without) and any child traversal 
producing multiple results,
+     * the predicate SHALL accept all results as the collection value.
+     * <p>
+     * <b>Validates: Requirements 2.5</b>
+     */
+    public static class SingleValuePredicateRejectionTest {
+
+        private Traverser.Admin<?> createTraverser(final Object value) {
+            return new B_O_Traverser<>(value, 1L);
+        }
+
+        // --- Single-value predicates should throw on multiple results ---
+
+        // --- Single-value predicates take first result, ignore extras 
(consistent with by()) ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForEq() {
+            final P<Object> p = P.eq(__.union(__.constant(1), 
__.constant(2)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForNeq() {
+            final P<Object> p = P.neq(__.union(__.constant(1), 
__.constant(2)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(false));
+            assertThat(p.test(2), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForGt() {
+            final P<Object> p = P.gt(__.union(__.constant(10), 
__.constant(20)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(11), is(true));
+            assertThat(p.test(10), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForLt() {
+            final P<Object> p = P.lt(__.union(__.constant(10), 
__.constant(20)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(9), is(true));
+            assertThat(p.test(10), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForGte() {
+            final P<Object> p = P.gte(__.union(__.constant(10), 
__.constant(20)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(10), is(true));
+            assertThat(p.test(9), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldTakeFirstResultForLte() {
+            final P<Object> p = P.lte(__.union(__.constant(10), 
__.constant(20)).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(10), is(true));
+            assertThat(p.test(11), is(false));
+        }
+
+        // --- Collection predicates should accept multiple results ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptMultipleResultsForWithin() {
+            // within(traversal) takes first result only. Use fold() to get a 
collection.
+            final P<Object> p = P.within(__.inject(1, 2, 3).fold().asAdmin());
+            p.resolve(createTraverser("start"));
+            // After resolve, the predicate should have the collection value 
and be testable
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(3), is(true));
+            assertThat(p.test(4), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptMultipleResultsForWithout() {
+            // without(traversal) takes first result only. Use fold() to get a 
collection.
+            final P<Object> p = P.without(__.inject(1, 2, 3).fold().asAdmin());
+            p.resolve(createTraverser("start"));
+            // After resolve, without should exclude the resolved values
+            assertThat(p.test(1), is(false));
+            assertThat(p.test(2), is(false));
+            assertThat(p.test(3), is(false));
+            assertThat(p.test(4), is(true));
+        }
+
+        // --- Single-value predicates should succeed with exactly one result 
---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptSingleResultForEq() {
+            final P<Object> p = P.eq(__.constant(42).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(42), is(true));
+            assertThat(p.test(99), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptSingleResultForNeq() {
+            final P<Object> p = P.neq(__.constant(42).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(42), is(false));
+            assertThat(p.test(99), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptSingleResultForGt() {
+            final P<Object> p = P.gt(__.constant(10).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(11), is(true));
+            assertThat(p.test(10), is(false));
+            assertThat(p.test(9), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptSingleResultForLt() {
+            final P<Object> p = P.lt(__.constant(10).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(9), is(true));
+            assertThat(p.test(10), is(false));
+            assertThat(p.test(11), is(false));
+        }
+
+        // --- Collection predicates should also work with single result ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldAcceptSingleResultForWithin() {
+            final P<Object> p = P.within(__.constant(42).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(42), is(true));
+            assertThat(p.test(99), is(false));
+        }
+    }
+
+    /**
+     * Tests for multi-traversal support in within() and without().
+     * <p>
+     * When multiple traversals are passed to within(trav1, trav2, ...), each 
traversal is evaluated
+     * independently and results are unioned into a single collection for the 
Contains test.
+     */
+    public static class MultiTraversalTest {
+
+        private Traverser.Admin<?> createTraverser(final Object value) {
+            return new B_O_Traverser<>(value, 1L);
+        }
+
+        // --- Detection ---
+
+        @Test
+        public void shouldDetectMultipleTraversalsInWithin() {
+            final P<Object> p = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            assertThat(p.hasTraversal(), is(true));
+        }
+
+        @Test
+        public void shouldDetectMultipleTraversalsInWithout() {
+            final P<Object> p = P.without(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            assertThat(p.hasTraversal(), is(true));
+        }
+
+        @Test
+        public void shouldReturnTraversalValuesListForMultiTraversal() {
+            final P<Object> p = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            assertThat(p.getTraversalValues() != null, is(true));
+            assertThat(p.getTraversalValues().size(), is(2));
+        }
+
+        @Test
+        public void shouldReturnNullTraversalValueForMultiTraversal() {
+            // Single traversalValue should be null when using multi-traversal 
form
+            final P<Object> p = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            assertThat(p.getTraversalValue() == null, is(true));
+        }
+
+        // --- Resolution and testing ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveMultipleTraversalsForWithin() {
+            // within(__.constant(1), __.constant(2)) should union results: 
[1, 2]
+            final P<Object> p = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(3), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveMultipleTraversalsForWithout() {
+            // without(__.constant(1), __.constant(2)) should union results: 
[1, 2]
+            final P<Object> p = P.without(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(false));
+            assertThat(p.test(2), is(false));
+            assertThat(p.test(3), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveMultipleTraversalsWithMultipleResultsEach() {
+            // within(__.inject(1,2).fold(), __.inject(3,4).fold()) - each 
fold() produces a list, unpacked into union
+            final P<Object> p = P.within(__.inject(1, 2).fold().asAdmin(), 
__.inject(3, 4).fold().asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(3), is(true));
+            assertThat(p.test(4), is(true));
+            assertThat(p.test(5), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldHandleEmptyResultFromOneTraversal() {
+            // within(__.inject(1,2).fold(), __.limit(0)) where second 
produces nothing
+            // Should still match on results from first traversal
+            final P<Object> p = P.within(__.inject(1, 2).fold().asAdmin(), 
__.limit(0).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.isResolvedEmpty(), is(false));
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(3), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldHandleAllEmptyResults() {
+            // within(__.limit(0), __.limit(0)) where both produce nothing. A 
collection predicate resolves to
+            // an empty set rather than flagging resolved-empty, so within() 
simply matches nothing.
+            final P<Object> p = P.within(__.limit(0).asAdmin(), 
__.limit(0).asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.isResolvedEmpty(), is(false));
+            assertThat(p.test(1), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveThreeTraversals() {
+            // within(__.constant("a"), __.constant("b"), __.constant("c"))
+            final P<Object> p = P.within(__.constant("a").asAdmin(), 
__.constant("b").asAdmin(), __.constant("c").asAdmin());
+            p.resolve(createTraverser("start"));
+            assertThat(p.test("a"), is(true));
+            assertThat(p.test("b"), is(true));
+            assertThat(p.test("c"), is(true));
+            assertThat(p.test("d"), is(false));
+        }
+
+        // --- Clone independence ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldCloneMultiTraversalPredicate() {
+            final P<Object> original = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin());
+            final P<Object> clone = original.clone();
+
+            // Clone should have independent traversal values
+            assertThat(clone.hasTraversal(), is(true));
+            assertThat(clone.getTraversalValues() != null, is(true));
+            assertThat(clone.getTraversalValues().size(), is(2));
+
+            // Modifying clone should not affect original
+            clone.resolve(createTraverser("start"));
+            assertThat(clone.test(1), is(true));
+            // Original should still be unresolved (literals empty)
+            assertThat(original.getTraversalValues().size(), is(2));
+        }
+
+        // --- Varargs detection ---
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldDetectMultipleTraversalsInVarargs() {
+            // This tests the varargs path: P.within(trav1, trav2) going 
through within(V... values)
+            final Traversal<?, ?> trav1 = __.constant(10);
+            final Traversal<?, ?> trav2 = __.constant(20);
+            final P<Object> p = (P<Object>) P.within(trav1, trav2);
+            assertThat(p.hasTraversal(), is(true));
+            assertThat(p.getTraversalValues() != null, is(true));
+            assertThat(p.getTraversalValues().size(), is(2));
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(10), is(true));
+            assertThat(p.test(20), is(true));
+            assertThat(p.test(30), is(false));
+        }
+
+        // --- collectTraversals and integrateTraversals ---
+
+        @Test
+        public void shouldCollectTraversalsFromMultiTraversalPredicate() {
+            final P<Object> p = P.within(__.constant(1).asAdmin(), 
__.constant(2).asAdmin(), __.constant(3).asAdmin());
+            final java.util.List<Traversal.Admin<?, ?>> collected = new 
java.util.ArrayList<>();
+            P.collectTraversals(p, collected);
+            assertThat(collected.size(), is(3));
+        }
+    }
+
+    /**
+     * Covers the empty-result semantics of collection (within/without) versus 
scalar predicates and the
+     * behavior of connective predicates (AndP/OrP) and NotP when their 
operands carry child traversals.
+     * <p>
+     * Regression coverage for: within(empty) -> false, without(empty) -> true 
(collection predicates resolve
+     * to an empty collection rather than short-circuiting), scalar predicates 
remain resolved-empty, and the
+     * shared-state safety of resolving the same predicate across many 
traversers sequentially.
+     */
+    public static class EmptyResolutionAndConnectiveTest {
+
+        private Traverser.Admin<?> createTraverser(final Object value) {
+            return new B_O_Traverser<>(value, 1L);
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldPassWithoutWhenTraversalResolvesEmpty() {
+            // without(empty set) -> nothing to exclude -> everything passes
+            final P<Object> p = P.without(__.<Object>limit(0).asAdmin());
+            p.resolve(createTraverser("anything"));
+            assertThat(p.isResolvedEmpty(), is(false));
+            assertThat(p.test("anything"), is(true));
+            assertThat(p.test(42), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldFailWithinWhenTraversalResolvesEmpty() {
+            // within(empty set) -> nothing matches -> everything fails
+            final P<Object> p = P.within(__.<Object>limit(0).asAdmin());
+            p.resolve(createTraverser("anything"));
+            assertThat(p.isResolvedEmpty(), is(false));
+            assertThat(p.test("anything"), is(false));
+            assertThat(p.test(42), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldPassWithoutWhenFoldedTraversalIsEmpty() {
+            // explicit empty collection via fold() of nothing
+            final P<Object> p = 
P.without(__.<Object>limit(0).fold().asAdmin());
+            p.resolve(createTraverser("anything"));
+            assertThat(p.test("anything"), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldFailWithinWhenMultiTraversalResolvesAllEmpty() {
+            final P<Object> p = P.within(__.<Object>limit(0).asAdmin(), 
__.<Object>limit(0).asAdmin());
+            p.resolve(createTraverser("anything"));
+            assertThat(p.isResolvedEmpty(), is(false));
+            assertThat(p.test(1), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void 
shouldRemainResolvedEmptyForScalarPredicateWithEmptyTraversal() {
+            // eq(empty) has no comparison value -> resolved empty so the step 
can short-circuit
+            final P<Object> p = P.eq(__.<Object>limit(0).asAdmin());
+            p.resolve(createTraverser("anything"));
+            assertThat(p.isResolvedEmpty(), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveAndPWithTraversalOperands() {
+            final P<Object> p = (P<Object>) (P) 
P.gt(__.constant(10).asAdmin()).and(P.lt(__.constant(20).asAdmin()));
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(15), is(true));
+            assertThat(p.test(10), is(false));
+            assertThat(p.test(25), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldShortCircuitAndPResolveWhenScalarChildEmpty() {
+            // first child resolves empty (no comparison value); AndP cannot 
be satisfied
+            final P<Object> p = (P<Object>) (P) 
P.eq(__.<Object>limit(0).asAdmin()).and(P.gt(__.constant(5).asAdmin()));
+            p.resolve(createTraverser("start"));
+            assertThat(p.isResolvedEmpty(), is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveOrPWithTraversalOperands() {
+            final P<Object> p = (P<Object>) (P) 
P.eq(__.constant(1).asAdmin()).or(P.eq(__.constant(2).asAdmin()));
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(1), is(true));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(3), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void 
shouldReturnFalseForOrPWhenResolvedNonEmptyButNonMatching() {
+            // a non-empty within() resolution that simply does not contain 
the test value must still return false
+            final P<Object> p = (P<Object>) (P) P.within(__.inject(1, 2, 
3).fold().asAdmin())
+                    .or(P.within(__.inject(4, 5, 6).fold().asAdmin()));
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(2), is(true));
+            assertThat(p.test(5), is(true));
+            assertThat(p.test(9), is(false));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void shouldResolveNotPWrappingTraversalPredicate() {
+            final P<Object> p = P.eq(__.constant(42).asAdmin()).negate();
+            p.resolve(createTraverser("start"));
+            assertThat(p.test(42), is(false));
+            assertThat(p.test(99), is(true));
+        }
+
+        @Test
+        public void shouldExposeWrappedPredicateFromNotP() {
+            final P<Object> inner = P.eq(__.constant(42).asAdmin());
+            final NotP<Object> notP = new NotP<>(inner);
+            assertThat(notP.getWrapped() == inner, is(true));
+        }
+
+        @SuppressWarnings("unchecked")
+        @Test
+        public void 
shouldProduceConsistentResultsAcrossManySequentialResolves() {
+            // resolve() mutates the predicate per traverser; verify repeated 
resolve+test cycles are stable
+            final P<Object> p = P.gt(__.constant(10).asAdmin());
+            for (int i = 0; i < 1000; i++) {
+                p.resolve(createTraverser("start" + i));
+                assertThat(p.test(11), is(true));
+                assertThat(p.test(5), is(false));
+            }
+        }
+    }
+}

Reply via email to