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

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

commit 84586ac66794a57031c16d1d4ae8b6e9e382b5d3
Author: Stephen Mallette <[email protected]>
AuthorDate: Tue Jun 30 12:33:26 2026 -0400

    Add is() and loops() to gremlin-javascript Tiny Gremlin
    
    loops() reports the enclosing repeat()'s iteration count and pairs with is()
    to bound a loop, e.g. until(__.loops().is(2)). Also restores the dropped
    Tiny Gremlin changelog entry.
    
    Assisted-by: Claude Code:claude-opus-4-8
---
 CHANGELOG.asciidoc                                 |  2 +
 docs/src/dev/provider/tiny-gremlin.asciidoc        | 30 +++++--
 .../lib/language/executor/ExecuteVisitor.ts        | 18 +++++
 .../lib/process/local/LocalExecutor.ts             | 28 +++++--
 .../gremlin-javascript/lib/process/local/passes.ts |  4 +-
 .../gremlin-javascript/lib/process/local/steps.ts  | 67 +++++++++++----
 .../gremlin-javascript/lib/process/local/types.ts  |  8 ++
 .../test/unit/tiny-gremlin-is-loops-test.js        | 94 ++++++++++++++++++++++
 .../gremlin/test/features/branch/Repeat.feature    |  1 +
 .../gremlin/test/features/filter/Is.feature        |  6 ++
 10 files changed, 230 insertions(+), 28 deletions(-)

diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index 963ad54f5c..85472872c1 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -130,6 +130,8 @@ 
image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
 * Added the `gql-gremlin` module containing the TinkerGQL engine, a 
`MATCH`-pattern executor implementing a deliberate minimal subset of ISO GQL, 
usable by any graph provider.
 * Added `Graph.countVerticesByLabel(String)`, 
`Graph.countEdgesByLabel(String)`, and the `Graph.Index` nested interface as 
performance-hint extension points for the TinkerGQL engine.
 * Added GQL support to TinkerGraph with `gql-gremlin`,
+* Added Tiny Gremlin semantics which provider a defined subset of Gremlin 
steps (navigation, filtering, value extraction, path tracking, range, ordering, 
looping, and local mutation) for use as an official lightweight Gremlin 
implementation.
+* Implemented Tiny Gremlin for Typescript.
 
 [[release-4-0-0-beta-2]]
 === TinkerPop 4.0.0-beta.2 (April 1, 2026)
diff --git a/docs/src/dev/provider/tiny-gremlin.asciidoc 
b/docs/src/dev/provider/tiny-gremlin.asciidoc
index 1bbb310827..c7a6f94dd5 100644
--- a/docs/src/dev/provider/tiny-gremlin.asciidoc
+++ b/docs/src/dev/provider/tiny-gremlin.asciidoc
@@ -42,7 +42,7 @@ deliberately excluded. The result is a small, self-contained, 
predictable execut
 
 === Supported Steps
 
-Tiny Gremlin supports exactly the 37 steps listed below, grouped by category. 
No other step may be used in a Tiny
+Tiny Gremlin supports exactly the 39 steps listed below, grouped by category. 
No other step may be used in a Tiny
 Gremlin traversal; doing so results in an error before any graph data is 
accessed.
 
 [width="100%",options="header"]
@@ -50,12 +50,12 @@ Gremlin traversal; doing so results in an error before any 
graph data is accesse
 |Category |Steps
 |Source |`V()`, `E()`
 |Navigation |`out()`, `in()`, `both()`, `outE()`, `inE()`, `bothE()`, 
`outV()`, `inV()`, `otherV()`
-|Filter |`has()`, `hasId()`, `hasLabel()`, `hasNot()`
+|Filter |`has()`, `hasId()`, `hasLabel()`, `hasNot()`, `is()`
 |Value Extraction |`id()`, `label()`, `values()`, `valueMap()`, 
`elementMap()`, `value()`, `key()`
 |Path |`path()`
 |Range |`limit()`, `range()`, `skip()`, `tail()`
 |Ordering |`order()`
-|Branch |`repeat()`
+|Branch |`repeat()`, `loops()`
 |Mutation |`addV()`, `addE()`, `property()`
 |Modulators |`from()`, `to()`, `times()`, `until()`, `emit()`
 |===
@@ -228,6 +228,14 @@ Maps a vertex to its incoming edges.
 
 Maps an edge to its incoming vertex (the head of the arrow).
 
+=== is()
+
+*Gremlin Semantics reference:* <<is-step,is()>>
+
+Filters the stream, keeping only traversers whose value equals the given 
object or satisfies the given predicate, for
+example `is(32)` or `is(P.gte(29))`. It pairs naturally with `loops()` to 
bound a `repeat()`, as in
+`until(__.loops().is(2))`.
+
 === key()
 
 *Gremlin Semantics reference:* <<key-step,key()>>
@@ -250,6 +258,18 @@ Truncates the result stream to at most `n` elements.
 
 * The `Scope` form is not supported and will raise an error.
 
+=== loops()
+
+*Gremlin Semantics reference:* <<loops-step,loops()>>
+
+Maps each traverser to the loop count of the nearest enclosing `repeat()` — 
zero on the first body pass, incrementing
+each iteration. It is most often used as an exit condition with `is()`, so 
`repeat(traversal).until(__.loops().is(2))`
+is equivalent to `repeat(traversal).times(2)`. Outside any `repeat()` it is 
zero.
+
+*Tiny Gremlin limitations:*
+
+* The `loops("label")` form, which references a labeled loop, is not supported 
because Tiny Gremlin has no step labels.
+
 === order()
 
 *Gremlin Semantics reference:* <<order-step,order()>>
@@ -365,8 +385,8 @@ modulator relative to `repeat()` determines its semantics, 
matching full Gremlin
   Declared after `repeat()` it emits traversers leaving the body, and declared 
before it emits them entering the body.
 
 A `condition` for `until()` or `emit()` is an anonymous filter traversal, 
satisfied when it produces at least one
-result. The loop body and these conditions may themselves contain any 
supported step, including `order().by()` or
-`path()`.
+result. The loop body and these conditions may themselves contain any 
supported step, including `order().by()`,
+`path()`, or `loops()` — so `until(__.loops().is(2))` bounds the loop by 
iteration count, equivalent to `times(2)`.
 
 The loop is driven breadth-first over the whole frontier: at each loop 
iteration every surviving traverser advances
 in lockstep through one shared evaluation of the body. A barrier step such as 
`order().by()` inside the body
diff --git 
a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts 
b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
index 84b77dc3e4..e593bef2a7 100644
--- a/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
+++ b/gremlin-js/gremlin-javascript/lib/language/executor/ExecuteVisitor.ts
@@ -516,6 +516,24 @@ export class ExecuteVisitor {
     this.push('emit', [this.recoverPredicate(ctx.traversalPredicate())]);
   }
 
+  visitTraversalMethod_loops_Empty(_ctx: any): void {
+    this.push('loops', []);
+  }
+
+  // loops(String) — the loop-label reference. Tiny Gremlin has no step 
labels, so the
+  // label is carried through for the executor to reject with a clear message.
+  visitTraversalMethod_loops_String(ctx: any): void {
+    this.push('loops', [this.recoverStringLiteral(ctx.stringLiteral())]);
+  }
+
+  visitTraversalMethod_is_Object(ctx: any): void {
+    this.push('is', [this.recoverGenericArg(ctx.genericArgument())]);
+  }
+
+  visitTraversalMethod_is_P(ctx: any): void {
+    this.push('is', [this.recoverPredicate(ctx.traversalPredicate())]);
+  }
+
   // ── Mutation steps ────────────────────────────────────────────────────────
 
   visitTraversalMethod_addV_Empty(ctx: any): void {
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts 
b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
index a37f25ebff..18090527c5 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/LocalExecutor.ts
@@ -24,9 +24,9 @@ import { Graph, Vertex, Edge } from 
'../../structure/graph.js';
 import {
   wrap, getValue, NON_PRODUCTIVE, type StreamItem,
   stepOut, stepIn, stepBoth, stepOutE, stepInE, stepBothE, stepOutV, stepInV, 
stepOtherV,
-  stepHas, stepHasId, stepHasLabel, stepHasNot,
+  stepHas, stepHasId, stepHasLabel, stepHasNot, stepIs,
   stepId, stepLabel, stepValue, stepKey, stepValues, stepValueMap, 
stepElementMap,
-  stepPath, stepRepeat,
+  stepPath, stepRepeat, stepLoops,
   stepLimit, stepRange, stepSkip, stepTail, stepOrder,
   stepAddV, stepAddEWithModulators, stepProperty,
 } from './steps.js';
@@ -183,9 +183,19 @@ export class LocalExecutor {
 
   /** Builds the ExecutionContext handed to parent steps for running their 
child pipelines. */
   private makeContext(graph: Graph, trackPaths: boolean): ExecutionContext {
+    return this.buildContext(graph, trackPaths, 0);
+  }
+
+  /**
+   * Builds a context at a specific loops() value. withLoops() returns a fresh 
context rather
+   * than mutating, so a nested repeat() can set its own loop count without 
clobbering the
+   * outer loop's — each level's loops() reads the value baked into its own 
context object.
+   */
+  private buildContext(graph: Graph, trackPaths: boolean, loops: number): 
ExecutionContext {
     const ctx: ExecutionContext = {
       graph,
       trackPaths,
+      loops,
       runBranch: (child, src) => {
         let stream: Iterable<StreamItem> = src;
         for (const step of child) stream = this.applyStep(step, stream, ctx);
@@ -193,17 +203,18 @@ export class LocalExecutor {
       },
       runProject: (child, object) => {
         // child is a single value-extraction step (validated during folding).
-        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        const probe = this.buildContext(graph, false, loops);
         for (const out of this.applyStep(child[0], [wrap(object, [], false)], 
probe)) {
           return getValue(out, false);
         }
         return NON_PRODUCTIVE; // non-productive by(Traversal) filters the 
path traverser
       },
       runRooted: (child) => {
-        const probe: ExecutionContext = { ...ctx, trackPaths: false };
+        const probe = this.buildContext(graph, false, loops);
         for (const item of this.buildChain(child, probe)) return item;
         return null;
       },
+      withLoops: (n) => this.buildContext(graph, trackPaths, n),
     };
     return ctx;
   }
@@ -329,9 +340,10 @@ function requireTraversalCondition(stepName: string, arg: 
Arg | undefined): Pipe
 }
 
 // ── Parent step registry 
────────────────────────────────────────────────────────
-// Steps that run nested child pipelines. They receive the ExecutionContext 
and use
-// its runBranch/runProject/runRooted helpers, so adding a new branching step 
is a
-// registry entry rather than a special case in applyStep.
+// Steps that need the ExecutionContext — to run nested child pipelines (path, 
addE,
+// repeat) or to read loop state (loops). They use its 
runBranch/runProject/runRooted
+// helpers and loops field, so adding such a step is a registry entry rather 
than a
+// special case in applyStep.
 
 const PARENT_STEPS = new Map<string, ParentStepFn>([
   ['path', (source, args, ctx) => stepPath(source, args, ctx.trackPaths, (sub, 
obj) => ctx.runProject(sub, obj))],
@@ -345,6 +357,7 @@ const PARENT_STEPS = new Map<string, ParentStepFn>([
     (sub) => ctx.runRooted(sub as unknown as Pipeline),
   )],
   ['repeat', (source, args, ctx) => stepRepeat(source, args[0] as unknown as 
RepeatSpec, ctx)],
+  ['loops', (source, args, ctx) => stepLoops(source, args, ctx)],
 ]);
 
 // ── Result copying 
────────────────────────────────────────────────────────────
@@ -426,6 +439,7 @@ const STEP_REGISTRY = new Map<string, StepFn>([
   ['hasId',       stepHasId],
   ['hasLabel',    stepHasLabel],
   ['hasNot',      stepHasNot],
+  ['is',          stepIs],
   ['id',          stepId],
   ['label',       stepLabel],
   ['value',       stepValue],
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts 
b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
index be4088837b..948d3915b2 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/passes.ts
@@ -26,12 +26,12 @@ export type OptimizationPass = (pipeline: Pipeline) => 
Pipeline;
 const SUPPORTED_STEPS = new Set<string>([
   'V', 'E',
   'out', 'in', 'both', 'outE', 'inE', 'bothE', 'outV', 'inV', 'otherV',
-  'has', 'hasId', 'hasLabel', 'hasNot',
+  'has', 'hasId', 'hasLabel', 'hasNot', 'is',
   'id', 'label', 'values', 'valueMap', 'elementMap', 'value', 'key',
   'path',
   'limit', 'range', 'skip', 'tail',
   'order', 'by',
-  'repeat', 'times', 'until', 'emit',
+  'repeat', 'times', 'until', 'emit', 'loops',
   'addV', 'addE', 'property',
   'from', 'to',
 ]);
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts 
b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
index 164accf753..70a3598e2c 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/steps.ts
@@ -329,6 +329,20 @@ export function* stepHasNot(
   }
 }
 
+export function* stepIs(
+  source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
+): Generator<StreamItem> {
+  const arg = args[0];
+  for (const item of source) {
+    const v = getValue(item, trackPaths);
+    if (arg instanceof P || arg instanceof TextP) {
+      if (evaluatePredicate(arg, v)) yield item;
+    } else if (v === arg) {
+      yield item;
+    }
+  }
+}
+
 export function* stepHasKey(
   source: Iterable<StreamItem>, args: Arg[], graph: Graph, trackPaths: boolean,
 ): Generator<StreamItem> {
@@ -767,6 +781,24 @@ export function* stepProperty(
   }
 }
 
+// ── Loops step 
────────────────────────────────────────────────────────────────
+
+/**
+ * Replaces each traverser's value with the loop count of the nearest 
enclosing repeat()
+ * (ctx.loops), typically tested by a following is(), e.g. 
until(__.loops().is(2)). Zero
+ * outside any repeat(). The loops("label") form is rejected — Tiny Gremlin 
has no step labels.
+ */
+export function* stepLoops(
+  source: Iterable<StreamItem>, args: Arg[], ctx: ExecutionContext,
+): Generator<StreamItem> {
+  if (args.length > 0) {
+    throw new Error('loops("label") is not supported in Tiny Gremlin; step 
labels are excluded');
+  }
+  for (const item of source) {
+    yield wrap(ctx.loops, getPath(item, ctx.trackPaths), ctx.trackPaths);
+  }
+}
+
 // ── Repeat step 
───────────────────────────────────────────────────────────────
 
 /**
@@ -795,47 +827,54 @@ export function* stepRepeat(
   ctx: ExecutionContext,
 ): Generator<StreamItem> {
   // until()/emit() conditions are always filter traversals here; the 
predicate form
-  // is rejected during folding (see requireTraversalCondition in 
LocalExecutor).
-  const conditionHolds = (cond: Pipeline, item: StreamItem): boolean => {
-    for (const _out of ctx.runBranch(cond, [item])) return true; // filter 
traversal: any output => true
+  // is rejected during folding (see requireTraversalCondition in 
LocalExecutor). Each is
+  // run through a context carrying the loop count it should observe via 
loops(), so
+  // until(__.loops().is(n)) sees the same counter that times(n) compares 
against.
+  const conditionHolds = (cond: Pipeline, item: StreamItem, condCtx: 
ExecutionContext): boolean => {
+    for (const _out of condCtx.runBranch(cond, [item])) return true; // filter 
traversal: any output => true
     return false;
   };
 
-  const exitBeforeBody = (item: StreamItem, loops: number): boolean => {
+  const exitBeforeBody = (item: StreamItem, loops: number, condCtx: 
ExecutionContext): boolean => {
     if (spec.times != null && spec.timesFirst && loops >= spec.times) return 
true;
-    if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, 
item)) return true;
+    if (spec.untilFirst && spec.until != null && conditionHolds(spec.until, 
item, condCtx)) return true;
     return false;
   };
-  const exitAfterBody = (item: StreamItem, loops: number): boolean => {
+  const exitAfterBody = (item: StreamItem, loops: number, condCtx: 
ExecutionContext): boolean => {
     if (spec.times != null && !spec.timesFirst && loops >= spec.times) return 
true;
-    if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, 
item)) return true;
+    if (!spec.untilFirst && spec.until != null && conditionHolds(spec.until, 
item, condCtx)) return true;
     return false;
   };
-  const shouldEmit = (item: StreamItem): boolean => {
+  const shouldEmit = (item: StreamItem, condCtx: ExecutionContext): boolean => 
{
     if (!spec.emitPresent) return false;
     if (spec.emit == null) return true; // bare emit() — emit every traverser
-    return conditionHolds(spec.emit, item);
+    return conditionHolds(spec.emit, item, condCtx);
   };
 
   let frontier: StreamItem[] = [...source];
   let loops = 0;
   while (frontier.length > 0) {
+    // The body, the while-phase until(), and an emit-before condition all 
observe the
+    // current loop count; the do-while until() and emit-after condition 
observe the
+    // post-body count.
+    const curCtx = ctx.withLoops(loops);
     // While-phase: traversers that hit a before-body exit leave the loop now;
     // the rest (optionally emitted ahead of the body) form the body's input.
     const entering: StreamItem[] = [];
     for (const item of frontier) {
-      if (exitBeforeBody(item, loops)) { yield item; continue; }
-      if (spec.emitFirst && shouldEmit(item)) yield item;
+      if (exitBeforeBody(item, loops, curCtx)) { yield item; continue; }
+      if (spec.emitFirst && shouldEmit(item, curCtx)) yield item;
       entering.push(item);
     }
     if (entering.length === 0) break;
 
     // Run the body over the entire frontier in one pass so barriers see it 
all.
     const nextLoops = loops + 1;
+    const nextCtx = ctx.withLoops(nextLoops);
     const nextFrontier: StreamItem[] = [];
-    for (const out of ctx.runBranch(spec.body, entering)) {
-      if (exitAfterBody(out, nextLoops)) { yield out; continue; }
-      if (!spec.emitFirst && shouldEmit(out)) yield out;
+    for (const out of curCtx.runBranch(spec.body, entering)) {
+      if (exitAfterBody(out, nextLoops, nextCtx)) { yield out; continue; }
+      if (!spec.emitFirst && shouldEmit(out, nextCtx)) yield out;
       nextFrontier.push(out);
     }
     frontier = nextFrontier;
diff --git a/gremlin-js/gremlin-javascript/lib/process/local/types.ts 
b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
index d1aee1bb83..9e11ff7286 100644
--- a/gremlin-js/gremlin-javascript/lib/process/local/types.ts
+++ b/gremlin-js/gremlin-javascript/lib/process/local/types.ts
@@ -67,12 +67,20 @@ export type Pipeline = StepDescriptor[];
 export interface ExecutionContext {
   graph: Graph;
   trackPaths: boolean;
+  /**
+   * The loop count of the nearest enclosing repeat(), read by loops(). Zero 
outside any
+   * repeat() and on a repeat()'s first body pass. repeat() supplies the value 
to its body
+   * and its until()/emit() conditions via withLoops().
+   */
+  loops: number;
   /** Apply a child pipeline as mid-traversal steps over an existing stream 
(flatMap per traverser). */
   runBranch(child: Pipeline, source: Iterable<any>): Iterable<any>;
   /** Run a single value-extraction child against one object, returning its 
first result or NON_PRODUCTIVE. */
   runProject(child: Pipeline, object: any): any;
   /** Run a child as a complete source-rooted pipeline (e.g. addE from/to), 
returning the first result. */
   runRooted(child: Pipeline): any;
+  /** Derive a context with a different loops() value, used by repeat() per 
loop iteration. */
+  withLoops(loops: number): ExecutionContext;
 }
 
 /**
diff --git 
a/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js 
b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js
new file mode 100644
index 0000000000..8a786ab6d5
--- /dev/null
+++ b/gremlin-js/gremlin-javascript/test/unit/tiny-gremlin-is-loops-test.js
@@ -0,0 +1,94 @@
+/*
+ *  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.
+ */
+
+import { assert } from 'chai';
+import anon from '../../lib/process/anonymous-traversal.js';
+import { statics as __ } from '../../lib/process/graph-traversal.js';
+import { P } from '../../lib/process/traversal.js';
+import { buildModernGraph } from '../cucumber/local-fixtures.js';
+
+/**
+ * Unit coverage for is() (value/predicate filter) and loops() (the enclosing 
repeat()'s
+ * loop counter) in the Tiny Gremlin local executor. loops() is exercised 
through repeat()
+ * since that is where it carries meaning; the two steps are paired because 
loops() is inert
+ * without a following is() to test the count.
+ */
+describe('Tiny Gremlin is() and loops()', () => {
+  let g;
+  const markoId = 1;
+
+  beforeEach(() => {
+    g = anon.traversal().with_(buildModernGraph().graph);
+  });
+
+  // ── is() ──────────────────────────────────────────────────────────────────
+
+  it('is(value) keeps only equal values', async () => {
+    const ages = await g.V().values('age').is(32).toList();
+    assert.deepEqual(ages, [32]);
+  });
+
+  it('is(P) filters by predicate', async () => {
+    const ages = await g.V().values('age').is(P.lte(30)).toList();
+    assert.sameMembers(ages, [27, 29]);
+  });
+
+  it('chains is() predicates as a conjunction', async () => {
+    const ages = await g.V().values('age').is(P.gte(29)).is(P.lt(34)).toList();
+    assert.sameMembers(ages, [29, 32]);
+  });
+
+  // ── loops() ───────────────────────────────────────────────────────────────
+
+  it('until(loops().is(n)) before repeat() is while-bounded like times(n)', 
async () => {
+    // condition tested before the body: exits once the current loop count 
reaches 2
+    const names = await 
g.V().until(__.loops().is(2)).repeat(__.out()).values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('until(loops().is(n)) after repeat() is do-while and matches times(n)', 
async () => {
+    const viaLoops = await 
g.V(markoId).repeat(__.out()).until(__.loops().is(2)).values('name').toList();
+    const viaTimes = await 
g.V(markoId).repeat(__.out()).times(2).values('name').toList();
+    assert.sameMembers(viaLoops, viaTimes);
+    assert.sameMembers(viaLoops, ['lop', 'ripple']);
+  });
+
+  it('reports the inner loop count inside a nested repeat()', async () => {
+    // inner repeat exits when ITS own loops() reaches 1 (one out() hop), then 
the outer
+    // repeat takes a second hop; loops() resolves to the nearest enclosing 
repeat().
+    const names = await g.V(markoId)
+      
.repeat(__.repeat(__.out()).until(__.loops().is(1))).until(__.loops().is(2))
+      .values('name').toList();
+    assert.sameMembers(names, ['lop', 'ripple']);
+  });
+
+  it('is zero outside any repeat()', async () => {
+    const loops = await g.V().loops().toList();
+    assert.deepEqual(loops, [0, 0, 0, 0, 0, 0]);
+  });
+
+  it('rejects the loops("label") reference (no step labels in Tiny Gremlin)', 
async () => {
+    try {
+      await g.V(markoId).repeat(__.out()).until(__.loops('a').is(2)).toList();
+      assert.fail('expected a rejection for loops("label")');
+    } catch (err) {
+      assert.match(err.message, /loops\("label"\) is not supported/);
+    }
+  });
+});
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 a5652a312b..6364afefa3 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
@@ -951,6 +951,7 @@ Feature: Step - repeat()
       | josh |
       | peter |
 
+  @TinyGremlin
   Scenario: g_V_untilXloops_isX2XX_repeatXout_order_byXnameXX_valuesXnameX
     Given the modern graph
     And the traversal of
diff --git 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
index 93afd854b9..95e556cc9b 100644
--- 
a/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
+++ 
b/gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/test/features/filter/Is.feature
@@ -18,6 +18,7 @@
 @StepClassFilter @StepIs
 Feature: Step - is()
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isX32X
     Given the modern graph
     And the traversal of
@@ -29,6 +30,7 @@ Feature: Step - is()
       | result |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isX32varX
     Given the modern graph
     And using the parameter xx1 defined as "d[32].i"
@@ -41,6 +43,7 @@ Feature: Step - is()
       | result |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXlte_30X
     Given the modern graph
     And the traversal of
@@ -53,6 +56,7 @@ Feature: Step - is()
       | d[27].i |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXlte_30varX
     Given the modern graph
     And using the parameter xx1 defined as "d[30].i"
@@ -66,6 +70,7 @@ Feature: Step - is()
       | d[27].i |
       | d[29].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXgte_29X_isXlt_34X
     Given the modern graph
     And the traversal of
@@ -78,6 +83,7 @@ Feature: Step - is()
       | d[29].i |
       | d[32].i |
 
+  @TinyGremlin
   Scenario: g_V_valuesXageX_isXgte_29vaarX_isXlt_34varX
     Given the modern graph
     And using the parameter xx1 defined as "d[29].i"

Reply via email to