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

spmallette pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git

commit a20f3861f793bc465e83a3e1245a56024e2de3fa
Author: Stephen Mallette <stepm...@amazon.com>
AuthorDate: Thu Aug 21 12:43:03 2025 -0400

    Tests for repeat().times(0) and semantics clarity CTR
---
 docs/src/dev/provider/gremlin-semantics.asciidoc   | 73 ++++++++++++++++++++++
 .../gremlin/structure/service/ServiceRegistry.java |  1 -
 .../Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs |  4 ++
 gremlin-go/driver/cucumber/gremlin.go              |  4 ++
 .../gremlin-javascript/test/cucumber/gremlin.js    |  4 ++
 gremlin-python/src/main/python/radish/gremlin.py   |  4 ++
 .../gremlin/test/features/branch/Repeat.feature    | 46 +++++++++++++-
 7 files changed, 134 insertions(+), 2 deletions(-)

diff --git a/docs/src/dev/provider/gremlin-semantics.asciidoc 
b/docs/src/dev/provider/gremlin-semantics.asciidoc
index 1ef5dfe65d..399ab1d5b7 100644
--- a/docs/src/dev/provider/gremlin-semantics.asciidoc
+++ b/docs/src/dev/provider/gremlin-semantics.asciidoc
@@ -1380,6 +1380,79 @@ applies to list types which means that non-iterable 
types (including null) will
 See: 
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/map/ProductStep.java[source],
 link:https://tinkerpop.apache.org/docs/x.y.z/reference/#product-step[reference]
 
+[[repeat-step]]
+=== repeat()
+
+*Description:* Iteratively applies a traversal (the "loop body") to each 
incoming traverser until a stopping
+condition is met. Optionally, it can emit traversers on each iteration 
according to an emit predicate. The
+repeat step supports loop naming and a loop counter via `loops()`.
+
+*Syntax:* `repeat(Traversal repeatTraversal)` | `repeat(String loopName, 
Traversal repeatTraversal)`
+
+[width="100%",options="header"]
+|=========================================================
+|Start Step |Mid Step |Modulated |Domain |Range
+|N |Y |`emit()`, `until()`, `times()` |`any` |`any`
+|=========================================================
+
+*Arguments:*
+
+* `repeatTraversal` - The traversal that represents the loop body to apply on 
each iteration.
+* `loopName` - Optional name used to identify the loop for nested loops and to 
access a specific counter via
+`loops(loopName)`.
+
+*Modulation:*
+
+* `emit()` | `emit(Traversal<?, ?> emitTraversal)` | 
`emit(Predicate<Traverser<?>> emitPredicate)` - Controls if/when a
+  traverser is emitted to the downstream of repeat() in addition to being 
looped again. If supplied before `repeat(...)`
+  the predicate is evaluated prior to the first iteration (pre-emit). If 
supplied after `repeat(...)`, the predicate is
+  evaluated after each completed iteration (post-emit). Calling `emit()` 
without arguments is equivalent to a predicate
+  that always evaluates to true at the given check position.
+* `until(Traversal<?, ?> untilTraversal)` | `until(Predicate<Traverser<?>> 
untilPredicate)` - Controls when repetition
+  stops. If supplied before `repeat(...)` the predicate is evaluated prior to 
the first iteration (pre-check). If the
+  predicate is true, the traverser will pass downstream without any loop 
iteration. If supplied after `repeat(...)`, the
+  predicate is evaluated after each completed iteration (post-check). When the 
predicate is true, the traverser stops
+  repeating and passes downstream.
+* `times(int n)` - Convenience for a loop bound. Equivalent to 
`until(loops().is(n))` when placed after `repeat(...)`
+  (post-check), and equivalent to `until(loops().is(n))` placed before 
`repeat(...)` (pre-check) when specified before.
+  See Considerations for details and examples.
+
+*Considerations:*
+
+- Evaluation order matters. The placement of `emit()` and `until()` relative 
to `repeat()` controls whether their
+predicates are evaluated before the first iteration (pre) or after each 
iteration (post) allowing for `while/do` or
+`do/while` semantics respectively:
+  - Pre-check / pre-emit: when the modulator appears before `repeat(...)`.
+  - Post-check / post-emit: when the modulator appears after `repeat(...)`.
+- Loop counter semantics:
+  - The loop counter for a given named or unnamed repeat is incremented once 
per completion of the loop body (i.e.,
+after the body finishes), not before. Therefore, `loops()` reflects the number 
of completed iterations.
+  - `loops()` without arguments returns the counter for the closest 
(innermost) `repeat()`. `loops("name")` returns the
+counter for the named loop.
+- Re-queuing for the next iteration:
+  - After each iteration, if `until` is not satisfied at the post-check, the 
traverser is sent back into the loop body
+for another iteration. If it is satisfied, the traverser exits the loop and 
proceeds downstream.
+- Interaction of `times(n)`:
+  - `g.V().repeat(x).times(2)` applies `x` exactly twice; no values are 
emitted unless `emit()` is specified.
+  - `g.V().emit().repeat(x).times(2)` emits the original input (pre-emit) and 
then the results of each iteration.
+  - Placing `times(0)` before `repeat(...)` yields no iterations and passes 
the input downstream unchanged.
+  - Placing `times(0)` after `repeat(...)` yields the same as `times(1)` 
because of `do/while` semantics.
+- Errors when `repeatTraversal` is missing:
+  - Using `emit()`, `until()`, or `times()` without an associated `repeat()` 
will raise an error at iteration time with a
+message containing: `The repeat()-traversal was not defined`.
+- Nested repeats and loop names:
+  - Nested `repeat()` steps maintain separate loop counters. Use `repeat("a", 
...)` and `loops("a")` to reference a
+    specific counter inside nested loops.
+
+*Exceptions*
+
+* Using `emit()`, `until()`, or `times()` without a matching `repeat()` will 
raise an `IllegalStateException` at runtime
+  when the step is initialized during iteration with the message containing: 
`The repeat()-traversal was not defined`.
+
+See: 
link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/process/traversal/step/branch/RepeatStep.java[source],
+link:https://tinkerpop.apache.org/docs/x.y.z/reference/#repeat-step[reference],
+link:https://github.com/apache/tinkerpop/tree/x.y.z/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature[tests]
+
 [[replace-step]]
 === replace()
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
index aae9bed8bf..8aba51bd3c 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/service/ServiceRegistry.java
@@ -25,7 +25,6 @@ import 
org.apache.tinkerpop.gremlin.structure.util.CloseableIterator;
 import org.apache.tinkerpop.shaded.jackson.core.JsonProcessingException;
 import org.apache.tinkerpop.shaded.jackson.databind.ObjectMapper;
 
-import java.util.HashMap;
 import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Objects;
diff --git a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs 
b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
index 47c8064581..289228fe00 100644
--- a/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
+++ b/gremlin-dotnet/test/Gremlin.Net.IntegrationTest/Gherkin/Gremlin.cs
@@ -119,6 +119,10 @@ namespace Gremlin.Net.IntegrationTest.Gherkin
                {"g_V_emit", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Emit()}}, 
                {"g_V_untilXidentityX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.V().Until(__.Identity())}}, 
                {"g_V_timesX5X", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) =>g.V().Times(5)}}, 
+               
{"g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) 
=>g.V().Has("person","name","marko").Repeat(__.Out("created")).Times(1).Values<object>("name")}},
 
+               
{"g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) 
=>g.V().Has("person","name","marko").Repeat(__.Out("created")).Times(0).Values<object>("name")}},
 
+               
{"g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) 
=>g.V().Has("person","name","marko").Times(1).Repeat(__.Out("created")).Values<object>("name")}},
 
+               
{"g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) 
=>g.V().Has("person","name","marko").Times(0).Repeat(__.Out("created")).Values<object>("name")}},
 
                {"g_unionXX", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) =>g.Union<object>()}}, 
                {"g_unionXV_name", new List<Func<GraphTraversalSource, 
IDictionary<string, object>, ITraversal>> {(g,p) 
=>g.Union<object>(__.V().Values<object>("name"))}}, 
                {"g_unionXVXv1X_VX4XX_name", new 
List<Func<GraphTraversalSource, IDictionary<string, object>, ITraversal>> 
{(g,p) =>g.Union<object>(__.V((Vertex) p["v1"]),__.V((Vertex) 
p["v4"])).Values<object>("name")}}, 
diff --git a/gremlin-go/driver/cucumber/gremlin.go 
b/gremlin-go/driver/cucumber/gremlin.go
index 27cfb0aa58..1ce169f7f5 100644
--- a/gremlin-go/driver/cucumber/gremlin.go
+++ b/gremlin-go/driver/cucumber/gremlin.go
@@ -90,6 +90,10 @@ var translationMap = map[string][]func(g 
*gremlingo.GraphTraversalSource, p map[
     "g_V_emit": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Emit()}}, 
     "g_V_untilXidentityX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.V().Until(gremlingo.T__.Identity())}}, 
     "g_V_timesX5X": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.V().Times(5)}}, 
+    "g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("person", "name", 
"marko").Repeat(gremlingo.T__.Out("created")).Times(1).Values("name")}}, 
+    "g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("person", "name", 
"marko").Repeat(gremlingo.T__.Out("created")).Times(0).Values("name")}}, 
+    "g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("person", "name", 
"marko").Times(1).Repeat(gremlingo.T__.Out("created")).Values("name")}}, 
+    "g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name": {func(g 
*gremlingo.GraphTraversalSource, p map[string]interface{}) 
*gremlingo.GraphTraversal {return g.V().Has("person", "name", 
"marko").Times(0).Repeat(gremlingo.T__.Out("created")).Values("name")}}, 
     "g_unionXX": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return g.Union()}}, 
     "g_unionXV_name": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Union(gremlingo.T__.V().Values("name"))}}, 
     "g_unionXVXv1X_VX4XX_name": {func(g *gremlingo.GraphTraversalSource, p 
map[string]interface{}) *gremlingo.GraphTraversal {return 
g.Union(gremlingo.T__.V(p["v1"]), gremlingo.T__.V(p["v4"])).Values("name")}}, 
diff --git 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
index 6d6847a0fc..a15710cfb1 100644
--- 
a/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
+++ 
b/gremlin-javascript/src/main/javascript/gremlin-javascript/test/cucumber/gremlin.js
@@ -110,6 +110,10 @@ const gremlins = {
     g_V_emit: [function({g}) { return g.V().emit() }], 
     g_V_untilXidentityX: [function({g}) { return g.V().until(__.identity()) 
}], 
     g_V_timesX5X: [function({g}) { return g.V().times(5) }], 
+    g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name: 
[function({g}) { return 
g.V().has("person","name","marko").repeat(__.out("created")).times(1).values("name")
 }], 
+    g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name: 
[function({g}) { return 
g.V().has("person","name","marko").repeat(__.out("created")).times(0).values("name")
 }], 
+    g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name: 
[function({g}) { return 
g.V().has("person","name","marko").times(1).repeat(__.out("created")).values("name")
 }], 
+    g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name: 
[function({g}) { return 
g.V().has("person","name","marko").times(0).repeat(__.out("created")).values("name")
 }], 
     g_unionXX: [function({g}) { return g.union() }], 
     g_unionXV_name: [function({g}) { return g.union(__.V().values("name")) }], 
     g_unionXVXv1X_VX4XX_name: [function({g, v4, v1}) { return 
g.union(__.V(v1),__.V(v4)).values("name") }], 
diff --git a/gremlin-python/src/main/python/radish/gremlin.py 
b/gremlin-python/src/main/python/radish/gremlin.py
index 4fd1a36a45..9601ce5ee6 100644
--- a/gremlin-python/src/main/python/radish/gremlin.py
+++ b/gremlin-python/src/main/python/radish/gremlin.py
@@ -92,6 +92,10 @@ world.gremlins = {
     'g_V_emit': [(lambda g:g.V().emit())], 
     'g_V_untilXidentityX': [(lambda g:g.V().until(__.identity()))], 
     'g_V_timesX5X': [(lambda g:g.V().times(5))], 
+    'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name': [(lambda 
g:g.V().has('person','name','marko').repeat(__.out('created')).times(1).name)], 
+    'g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name': [(lambda 
g:g.V().has('person','name','marko').repeat(__.out('created')).times(0).name)], 
+    'g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name': [(lambda 
g:g.V().has('person','name','marko').times(1).repeat(__.out('created')).name)], 
+    'g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name': [(lambda 
g:g.V().has('person','name','marko').times(0).repeat(__.out('created')).name)], 
     'g_unionXX': [(lambda g:g.union())], 
     'g_unionXV_name': [(lambda g:g.union(__.V().name))], 
     'g_unionXVXv1X_VX4XX_name': [(lambda g, 
v4=None,v1=None:g.union(__.V(v1),__.V(v4)).name)], 
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
index 113ab6adac..ba95f1c45a 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/branch/Repeat.feature
@@ -397,4 +397,48 @@ Feature: Step - repeat()
       g.V().times(5)
       """
     When iterated to list
-    Then the traversal will raise an error with message containing text of 
"The repeat()-traversal was not defined"
\ No newline at end of file
+    Then the traversal will raise an error with message containing text of 
"The repeat()-traversal was not defined"
+
+  Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX1X_name
+    Given the modern graph
+    And the traversal of
+      """
+      
g.V().has("person","name","marko").repeat(__.out("created")).times(1).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | lop |
+
+  Scenario: g_V_haxXperson_name_markoX_repeatXoutXcreatedXX_timesX0X_name
+    Given the modern graph
+    And the traversal of
+      """
+      
g.V().has("person","name","marko").repeat(out("created")).times(0).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | lop |
+
+  Scenario: g_V_haxXperson_name_markoX_timesX1X_repeatXoutXcreatedXX_name
+    Given the modern graph
+    And the traversal of
+      """
+      
g.V().has("person","name","marko").times(1).repeat(out("created")).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | lop |
+
+  Scenario: g_V_haxXperson_name_markoX_timesX0X_repeatXoutXcreatedXX_name
+    Given the modern graph
+    And the traversal of
+      """
+      
g.V().has("person","name","marko").times(0).repeat(out("created")).values("name")
+      """
+    When iterated to list
+    Then the result should be unordered
+      | result |
+      | marko |
\ No newline at end of file

Reply via email to