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

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


The following commit(s) were added to refs/heads/csr by this push:
     new 1b08cffe1a cgc updates
1b08cffe1a is described below

commit 1b08cffe1afa9bc9d73154e500ad913746990e62
Author: Ken Hu <[email protected]>
AuthorDate: Thu Oct 8 09:35:48 2026 -0700

    cgc updates
---
 .../structure/snapshot/graph/CsrGraphComputer.java | 36 +++++--------
 .../structure/snapshot/process/op/CsrOp.java       |  8 +--
 .../structure/snapshot/process/op/CsrPlan.java     | 30 +++++++----
 .../gremlin/structure/snapshot/process/op/Ops.java |  7 +--
 .../structure/snapshot/process/op/Sources.java     |  1 -
 .../structure/snapshot/process/op/Terminals.java   | 13 ++---
 .../snapshot/process/op/value/MapOperators.java    | 60 ++++++++++++++++++++++
 .../snapshot/process/op/value/MapOps.java          | 52 +++++++++++++++++--
 .../process/op/value/ProjectKeyReader.java         |  3 +-
 .../snapshot/process/op/value/ValueOperators.java  | 10 +++-
 .../snapshot/process/strategy/PlanBuilder.java     | 24 ++++-----
 .../snapshot/process/strategy/Planner.java         |  2 +-
 .../snapshot/process/strategy/StepCompiler.java    | 34 +++++++++---
 spark-gremlin/src/test/csr-memory/csrbench.py      |  2 +-
 .../tinkerpop/gremlin/spark/csr/CsrMemoryRun.java  | 21 +++++---
 .../tinkerpop/gremlin/spark/csr/csr-queries.txt    | 34 +++++++-----
 16 files changed, 245 insertions(+), 92 deletions(-)

diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/graph/CsrGraphComputer.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/graph/CsrGraphComputer.java
index 495bd98144..cf9cc8b5de 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/graph/CsrGraphComputer.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/graph/CsrGraphComputer.java
@@ -49,6 +49,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.util.ProfileStep;
 import 
org.apache.tinkerpop.gremlin.process.traversal.traverser.util.TraverserSet;
 import org.apache.tinkerpop.gremlin.process.traversal.util.PureTraversal;
 import 
org.apache.tinkerpop.gremlin.process.traversal.util.TraversalInterruptedException;
+import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalHelper;
 import org.apache.tinkerpop.gremlin.process.traversal.util.TraversalUtil;
 import org.apache.tinkerpop.gremlin.structure.Direction;
 import org.apache.tinkerpop.gremlin.structure.Edge;
@@ -63,6 +64,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -111,12 +113,6 @@ public final class CsrGraphComputer implements 
GraphComputer {
     private static final String SHORTEST_DISTANCE = 
"gremlin.shortestPathVertexProgram.distanceTraversal";
     private static final String SHORTEST_MAX_DISTANCE = 
"gremlin.shortestPathVertexProgram.maxDistance";
     private static final String SHORTEST_INCLUDE_EDGES = 
"gremlin.shortestPathVertexProgram.includeEdges";
-    private static final String TRAVERSAL_VOTE_TO_HALT = 
"gremlin.traversalVertexProgram.voteToHalt";
-    private static final String TRAVERSAL_MUTATED_MEMORY_KEYS =
-            "gremlin.traversalVertexProgram.mutatedMemoryKeys";
-    private static final String TRAVERSAL_COMPLETED_BARRIERS =
-            "gremlin.traversalVertexProgram.completedBarriers";
-
     private final CsrGraph graph;
     private final ExecutorService computerService = 
Executors.newSingleThreadExecutor(r -> {
         final Thread thread = new Thread(r, 
CsrGraphComputer.class.getSimpleName() + "-boss");
@@ -192,9 +188,8 @@ public final class CsrGraphComputer implements 
GraphComputer {
         }
 
         GraphComputerHelper.validateProgramOnComputer(this, vertexProgram);
-        if (!vertexProgram.getMapReducers().isEmpty())
+        if (!(vertexProgram instanceof TraversalVertexProgram) && 
!vertexProgram.getMapReducers().isEmpty())
             throw new UnsupportedOperationException("CsrGraphComputer does not 
support VertexProgram MapReduce jobs");
-        if (vertexProgram instanceof TraversalVertexProgram) 
validateTraversalContinuation(vertexProgram);
 
         resultGraph = 
GraphComputerHelper.getResultGraphState(Optional.of(vertexProgram),
                 Optional.ofNullable(resultGraph));
@@ -305,20 +300,6 @@ public final class CsrGraphComputer implements 
GraphComputer {
                 || program instanceof TraversalVertexProgram;
     }
 
-    private static void validateTraversalContinuation(final VertexProgram<?> 
program) {
-        program.getMemoryComputeKeys().forEach(key -> {
-            final String name = key.getKey();
-            if (!name.equals(TraversalVertexProgram.HALTED_TRAVERSERS)
-                    && !name.equals(TraversalVertexProgram.ACTIVE_TRAVERSERS)
-                    && !name.equals(TRAVERSAL_VOTE_TO_HALT)
-                    && !name.equals(TRAVERSAL_MUTATED_MEMORY_KEYS)
-                    && !name.equals(TRAVERSAL_COMPLETED_BARRIERS)) {
-                throw new UnsupportedOperationException("CsrGraphComputer does 
not support traversal memory side "
-                        + "effects in TraversalVertexProgram: " + name);
-            }
-        });
-    }
-
     @Override
     public Features features() {
         return new Features() {
@@ -768,6 +749,7 @@ public final class CsrGraphComputer implements 
GraphComputer {
             final Traversal.Admin<Object, Object> traversal =
                     (Traversal.Admin<Object, Object>) 
program.getTraversal().getPure();
             traversal.setGraph(graph);
+            restoreGraphScans(traversal);
             final boolean returnHaltedTraversers = 
returnsHaltedTraversers(traversal);
             if (!returnHaltedTraversers && !retainByVertex)
                 throw new UnsupportedOperationException("A chained 
TraversalVertexProgram requires persisted vertex "
@@ -821,6 +803,16 @@ public final class CsrGraphComputer implements 
GraphComputer {
             return new TraversalResult(halted, haltedByVertex == null ? null : 
new ObjectColumn(haltedByVertex));
         }
 
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        private void restoreGraphScans(final Traversal.Admin<?, ?> traversal) {
+            for (final GraphStep graphStep :
+                    
TraversalHelper.getStepsOfAssignableClassRecursively(GraphStep.class, 
traversal)) {
+                graphStep.setIteratorSupplier(() -> (Iterator) 
(graphStep.returnsVertex()
+                        ? graph.vertices(graphStep.getIds())
+                        : graph.edges(graphStep.getIds())));
+            }
+        }
+
         private boolean returnsHaltedTraversers(final Traversal.Admin<?, ?> 
traversal) {
             final Step<?, ?> next = 
traversal.getParent().asStep().getNextStep();
             return next instanceof ComputerResultStep || next instanceof 
EmptyStep
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrOp.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrOp.java
index 91d116675c..924b89e4ca 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrOp.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrOp.java
@@ -23,10 +23,12 @@ import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.Lane;
 import java.util.List;
 
 /**
- * A node of the native operator IR, see the grammar in section 2.2 of the 
spike document:
+ * A node of the native operator IR:
  * <pre>
- * Plan := Source Op* Terminal?
+ * Plan := Source Node*
  * </pre>
+ * A barrier {@link Terminal} consumes its whole input and may either end a 
plan or feed later nodes. Reducing
+ * terminals emit one {@link Lane#SCALAR} entry; ordering terminals preserve 
their input lane.
  * Nodes are immutable descriptors built by the strategy; they hold no state 
and no reference to a graph. The nodes are
  * grouped in {@link Sources}, {@link Ops} and {@link Terminals}, with the key 
forms in {@link Keys} and the predicate
  * forms in {@link Preds}. Code that executes a node never switches on its 
class: it is created through the
@@ -101,7 +103,7 @@ public interface CsrOp {
     }
 
     /**
-     * A node that ends a plan: it consumes its whole input and emits a 
reduced result, or nothing.
+     * A barrier node: it consumes its whole input before emitting results.
      */
     interface Terminal extends CsrOp {
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrPlan.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrPlan.java
index 1332dcc61c..8fe6e75461 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrPlan.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/CsrPlan.java
@@ -26,9 +26,10 @@ import java.util.List;
 import java.util.Objects;
 
 /**
- * An immutable operator plan: a {@link CsrOp.Source}, then any number of 
ordinary nodes, then an optional
- * {@link CsrOp.Terminal}. Lanes are checked on construction by threading 
{@link CsrOp#outputLane(Lane)} through the
- * nodes, so a plan that exists is lane-consistent. A plan that starts with 
{@link Sources.Input} reads upstream
+ * An immutable operator plan: a {@link CsrOp.Source}, then any number of 
nodes. A {@link CsrOp.Terminal} may be
+ * followed by ordinary nodes; a reducer's single {@link Lane#SCALAR} result 
or an ordering terminal's original lane
+ * becomes the next node's input. Lanes are checked on construction by 
threading {@link CsrOp#outputLane(Lane)} through
+ * the nodes, so a plan that exists is lane-consistent. A plan that starts 
with {@link Sources.Input} reads upstream
  * traversers (at the top level) or its parent's current batch (as a child).
  */
 public final class CsrPlan {
@@ -38,7 +39,7 @@ public final class CsrPlan {
     private final List<Boolean> sources;
 
     /**
-     * @param nodes the source, the middle nodes and optionally the terminal, 
in order
+     * @param nodes the source and following nodes, in order
      * @throws IllegalArgumentException if the shape or the lanes are 
inconsistent
      */
     public CsrPlan(final List<? extends CsrOp> nodes) {
@@ -57,9 +58,6 @@ public final class CsrPlan {
             if (i > 0 && node instanceof CsrOp.Source && !(node instanceof 
Sources.MidScan)) {
                 throw new IllegalArgumentException(node.name() + " can only 
start a plan");
             }
-            if (node instanceof CsrOp.Terminal && i != this.nodes.size() - 1) {
-                throw new IllegalArgumentException(node.name() + " can only 
end a plan");
-            }
             if (node instanceof Ops.OtherV && !source) {
                 throw new IllegalArgumentException("OtherV needs edges that 
recorded their source vertex");
             }
@@ -88,14 +86,15 @@ public final class CsrPlan {
     }
 
     /**
-     * The nodes between the source and the terminal.
+     * Every node after the source except an ending terminal. A terminal 
followed by another node is included.
      */
     public List<CsrOp> ops() {
         return nodes.subList(1, terminal() == null ? nodes.size() : 
nodes.size() - 1);
     }
 
     /**
-     * The terminal, or null if the plan has none.
+     * The ending terminal, or null when the last node is not a terminal. 
Intermediate terminals are available through
+     * {@link #ops()}.
      */
     public CsrOp.Terminal terminal() {
         final CsrOp last = nodes.get(nodes.size() - 1);
@@ -138,7 +137,18 @@ public final class CsrPlan {
     }
 
     /**
-     * Whether the plan ends in a terminal.
+     * Whether the plan ends in one of the reducers supported as a {@code 
group().by(valueTraversal)} value.
+     */
+    public boolean isGroupReducer() {
+        final CsrOp.Terminal terminal = terminal();
+        return terminal instanceof Terminals.Count || terminal instanceof 
Terminals.Fold
+                || terminal instanceof Terminals.Sum || terminal instanceof 
Terminals.Min
+                || terminal instanceof Terminals.Max || terminal instanceof 
Terminals.Mean;
+    }
+
+    /**
+     * Whether the plan ends in any terminal/barrier. The historical method 
name includes ordering terminals and drain,
+     * not only scalar reducers; use {@link #isGroupReducer()} when validating 
a group value plan.
      */
     public boolean isReducing() {
         return terminal() != null;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Ops.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Ops.java
index 6cac9e5373..921caed99e 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Ops.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Ops.java
@@ -912,8 +912,9 @@ public final class Ops {
             Objects.requireNonNull(sideEffectKey);
             Objects.requireNonNull(key);
             Objects.requireNonNull(reducer);
-            if (reducer.inputLane() == null || !reducer.isReducing()) {
-                throw new IllegalArgumentException("A group reducer must start 
with Input and end in a terminal");
+            if (reducer.inputLane() == null || !reducer.isGroupReducer()) {
+                throw new IllegalArgumentException("A group reducer must start 
with Input and end in count, fold, "
+                        + "sum, min, max or mean");
             }
         }
 
@@ -956,6 +957,6 @@ public final class Ops {
         for (final CsrOp op : plan.ops()) {
             if (!op.isBulkLinear()) return false;
         }
-        return !plan.isReducing();
+        return plan.terminal() == null;
     }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Sources.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Sources.java
index 3588bbde34..740310b826 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Sources.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Sources.java
@@ -110,7 +110,6 @@ public final class Sources {
     public record Input(Lane lane) implements CsrOp.Source {
         public Input {
             Objects.requireNonNull(lane);
-            if (lane == Lane.SCALAR) throw new IllegalArgumentException("Input 
cannot read SCALAR");
         }
 
         @Override
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Terminals.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Terminals.java
index d35a2dda6c..96cd4e75f3 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Terminals.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/Terminals.java
@@ -27,10 +27,10 @@ import java.util.List;
 import java.util.Objects;
 
 /**
- * The nodes that end a plan: {@code Terminal := Count | Sum | Min | Max | 
Mean | GroupCount(key) | Group(key, Reducer) |
- * TopK(keys[], orders[], n, lo) | Sort(keys[], orders[]) | Fold | Drain}. A 
terminal consumes its whole input. Those
- * that emit one result emit it as a single {@code SCALAR} entry of bulk 1, 
and emit nothing for an empty input where
- * the standard step does (sum, min, max, mean).
+ * The barrier nodes: {@code Terminal := Count | Sum | Min | Max | Mean | 
GroupCount(key) | Group(key, Reducer) |
+ * TopK(keys[], orders[], n, lo) | Sort(keys[], orders[]) | Fold | Drain}. A 
terminal consumes its whole input before
+ * emitting results and may feed another node. Reducers emit one {@code 
SCALAR} entry of bulk 1, and emit nothing for
+ * an empty input where the standard step does (sum, min, max, mean).
  */
 public final class Terminals {
 
@@ -156,8 +156,9 @@ public final class Terminals {
         public Group {
             Objects.requireNonNull(key);
             Objects.requireNonNull(reducer);
-            if (reducer.inputLane() == null || !reducer.isReducing()) {
-                throw new IllegalArgumentException("A group reducer must start 
with Input and end in a terminal");
+            if (reducer.inputLane() == null || !reducer.isGroupReducer()) {
+                throw new IllegalArgumentException("A group reducer must start 
with Input and end in count, fold, "
+                        + "sum, min, max or mean");
             }
         }
 
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOperators.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOperators.java
index 16b947e95c..2e7603ea49 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOperators.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOperators.java
@@ -18,6 +18,8 @@
  */
 package org.apache.tinkerpop.gremlin.structure.snapshot.process.op.value;
 
+import org.apache.tinkerpop.gremlin.process.traversal.Path;
+import org.apache.tinkerpop.gremlin.process.traversal.step.util.Tree;
 import org.apache.tinkerpop.gremlin.process.traversal.step.util.WithOptions;
 import org.apache.tinkerpop.gremlin.structure.Direction;
 import org.apache.tinkerpop.gremlin.structure.T;
@@ -25,15 +27,21 @@ import 
org.apache.tinkerpop.gremlin.structure.snapshot.CsrSnapshot;
 import org.apache.tinkerpop.gremlin.structure.snapshot.graph.CsrGraph;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.Batch;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.Lane;
+import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.Materializer;
 import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.OperatorSpec;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.op.Keys;
 import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.op.nav.EntryStreamOperator;
+import org.apache.tinkerpop.gremlin.util.iterator.IteratorUtils;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.apache.tinkerpop.gremlin.util.NumberHelper.max;
+
 /**
  * The operators behind {@link MapOps}. Each builds one {@code LinkedHashMap} 
per input entry from the owner ranges and
  * columns, in the key order of the standard step, and emits it as a decoded 
{@code VAL} entry with the entry's bulk.
@@ -257,4 +265,56 @@ final class MapOperators {
             readers = null;
         }
     }
+
+    static final class SelectColumnOperator extends EntryStreamOperator {
+        private final MapOps.SelectColumn node;
+
+        SelectColumnOperator(final MapOps.SelectColumn node, final 
OperatorSpec spec) {
+            super(spec);
+            this.node = node;
+        }
+
+        @Override
+        protected boolean process(final Batch in, final int i, final Batch 
out) {
+            out.addValue(node.column().apply(Materializer.value(ctx, in, i)), 
in.bulk[i]);
+            return true;
+        }
+    }
+
+    static final class CountLocalOperator extends EntryStreamOperator {
+
+        CountLocalOperator(final OperatorSpec spec) {
+            super(spec);
+        }
+
+        @Override
+        protected boolean process(final Batch in, final int i, final Batch 
out) {
+            final Object item = Materializer.materialize(ctx, in, i);
+            final long count = item instanceof Tree ? ((Tree) item).nodeCount()
+                    : item instanceof Collection ? ((Collection<?>) 
item).size()
+                    : item instanceof Map ? ((Map<?, ?>) item).size()
+                    : item instanceof Path ? ((Path) item).size()
+                    : IteratorUtils.count(IteratorUtils.asIterator(item));
+            out.addValue(count, in.bulk[i]);
+            return true;
+        }
+    }
+
+    static final class MaxLocalOperator extends EntryStreamOperator {
+
+        MaxLocalOperator(final OperatorSpec spec) {
+            super(spec);
+        }
+
+        @Override
+        @SuppressWarnings({"rawtypes", "unchecked"})
+        protected boolean process(final Batch in, final int i, final Batch 
out) {
+            final Iterator<?> iterator = 
IteratorUtils.asIterator(Materializer.materialize(ctx, in, i));
+            if (!iterator.hasNext()) return true;
+            Comparable result = (Comparable) iterator.next();
+            while (iterator.hasNext()) result = max((Comparable) 
iterator.next(), result);
+            out.addValue(result, in.bulk[i]);
+            return true;
+        }
+    }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOps.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOps.java
index 8e7215ecd7..fa3aea6e22 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOps.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/MapOps.java
@@ -18,6 +18,7 @@
  */
 package org.apache.tinkerpop.gremlin.structure.snapshot.process.op.value;
 
+import org.apache.tinkerpop.gremlin.structure.Column;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.Lane;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.op.CsrOp;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.op.CsrPlan;
@@ -31,10 +32,10 @@ import java.util.Objects;
 
 /**
  * The IR nodes of the native map emission, which the shared IR in {@code Ops} 
has no form for: {@code valueMap()},
- * {@code propertyMap()}, {@code elementMap()} and {@code project()}. Each 
node turns an entry into one map and emits it
- * on the {@code VAL} lane with the entry's bulk; the maps are built directly 
from owner ranges and columns. Key codes
- * are vertex or edge key codes by the input lane; an empty array means every 
key in key-code order, and negative codes
- * (keys the snapshot does not have) are skipped. The planner must only emit a 
node after checking
+ * {@code propertyMap()}, {@code elementMap()}, {@code project()} and local 
maps over decoded values. Each node emits
+ * on the {@code VAL} lane with the entry's bulk. Element maps are built 
directly from owner ranges and columns. Key
+ * codes are vertex or edge key codes by the input lane; an empty array means 
every key in key-code order, and negative
+ * codes (keys the snapshot does not have) are skipped. The planner must only 
emit a node after checking
  * {@code CsrOperatorFactory#isImplemented}.
  */
 public final class MapOps {
@@ -141,7 +142,7 @@ public final class MapOps {
 
         @Override
         public Lane outputLane(final Lane input) {
-            requireLane(this, input, Lane.V, Lane.E, Lane.VP, Lane.EP, 
Lane.MP, Lane.VAL);
+            requireLane(this, input, Lane.V, Lane.E, Lane.VP, Lane.EP, 
Lane.MP, Lane.VAL, Lane.SCALAR);
             for (final Keys.Key key : keys) {
                 for (final CsrPlan child : key.children()) {
                     if (child.inputLane() != input) {
@@ -165,4 +166,45 @@ public final class MapOps {
             return "Project(" + names + "," + keys + ")";
         }
     }
+
+    /**
+     * {@code select(keys)} or {@code select(values)} over a decoded {@link 
java.util.Map},
+     * {@link java.util.Map.Entry} or {@link 
org.apache.tinkerpop.gremlin.process.traversal.Path}. The standard
+     * {@link Column} implementation defines both the accepted inputs and the 
returned collection/value.
+     */
+    public record SelectColumn(Column column) implements CsrOp {
+        public SelectColumn {
+            Objects.requireNonNull(column);
+        }
+
+        @Override
+        public Lane outputLane(final Lane input) {
+            requireLane(this, input, Lane.VAL, Lane.SCALAR);
+            return Lane.VAL;
+        }
+    }
+
+    /**
+     * {@code count(local)} over a decoded value. The operator follows {@code 
CountLocalStep}: collections, maps and
+     * paths use their sizes, trees use their recursive node count, and other 
values are viewed through
+     * {@code IteratorUtils.asIterator}.
+     */
+    public record CountLocal() implements CsrOp {
+        @Override
+        public Lane outputLane(final Lane input) {
+            Objects.requireNonNull(input);
+            return Lane.VAL;
+        }
+    }
+
+    /**
+     * {@code max(local)} over a decoded local value. Empty inputs are 
unproductive, as in the standard step.
+     */
+    public record MaxLocal() implements CsrOp {
+        @Override
+        public Lane outputLane(final Lane input) {
+            Objects.requireNonNull(input);
+            return Lane.VAL;
+        }
+    }
 }
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ProjectKeyReader.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ProjectKeyReader.java
index c599efbbed..018ea8864d 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ProjectKeyReader.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ProjectKeyReader.java
@@ -144,7 +144,8 @@ final class ProjectKeyReader implements AutoCloseable {
                 final Property<?> p = ((Element) Materializer.facade(ctx, b, 
i)).property(k.name());
                 return p.isPresent() ? p.value() : absent(k);
             }
-            case VAL: {
+            case VAL:
+            case SCALAR: {
                 final Object v = Materializer.value(ctx, b, i);
                 if (v instanceof Map) return ((Map<?, ?>) v).get(k.name());
                 throw new IllegalStateException(String.format("The by(\"%s\") 
modulator can only be applied to a "
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ValueOperators.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ValueOperators.java
index d7fbc767ca..344b45acaf 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ValueOperators.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/op/value/ValueOperators.java
@@ -30,8 +30,9 @@ import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.op.Ops;
  * <ul>
  *     <li>{@code Props}, {@code PropKey}, {@code PropValue}, {@code Id}, 
{@code Label}, {@code Labels},
  *     {@code Element} and {@code Constant} from the shared IR;</li>
- *     <li>{@link MapOps.ValueMap}, {@link MapOps.PropertyMap}, {@link 
MapOps.ElementMap} and {@link MapOps.Project},
- *     nodes that live in this package.</li>
+ *     <li>{@link MapOps.ValueMap}, {@link MapOps.PropertyMap}, {@link 
MapOps.ElementMap}, {@link MapOps.Project},
+ *     {@link MapOps.SelectColumn}, {@link MapOps.CountLocal} and {@link 
MapOps.MaxLocal}, nodes that live in this
+ *     package.</li>
  * </ul>
  */
 public final class ValueOperators implements OperatorRegistrar {
@@ -50,6 +51,11 @@ public final class ValueOperators implements 
OperatorRegistrar {
         factory.register(MapOps.PropertyMap.class, 
MapOperators.PropertyMapOperator::new);
         factory.register(MapOps.ElementMap.class, 
MapOperators.ElementMapOperator::new);
         factory.register(MapOps.Project.class, 
MapOperators.ProjectOperator::new);
+        factory.register(MapOps.SelectColumn.class, 
MapOperators.SelectColumnOperator::new);
+        factory.register(MapOps.CountLocal.class,
+                (node, spec) -> new MapOperators.CountLocalOperator(spec));
+        factory.register(MapOps.MaxLocal.class,
+                (node, spec) -> new MapOperators.MaxLocalOperator(spec));
 
         factory.registerEstimator(Ops.Props.class, (node, snapshot, in) -> {
             final double factor = node.keyCodes().length == 0 ? 
allKeys(snapshot) : node.keyCodes().length;
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/PlanBuilder.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/PlanBuilder.java
index f965309f2d..de4fc3e5df 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/PlanBuilder.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/PlanBuilder.java
@@ -27,15 +27,16 @@ import java.util.ArrayList;
 import java.util.List;
 
 /**
- * The nodes of a plan under construction, with the lane after the last node. 
A terminal closes the builder. Copies are
- * cheap, so a composite rule can build on a copy and commit it only when 
every piece compiled.
+ * The nodes of a plan under construction, with the lane after the last node. 
A compatible node may continue from a
+ * terminal's output. Copies are cheap, so a composite rule can build on a 
copy and commit it only when every piece
+ * compiled.
  */
 final class PlanBuilder {
 
     private final List<CsrOp> nodes = new ArrayList<>();
     private Lane lane;
     private boolean recordsSource;
-    private boolean closed;
+    private boolean hardClosed;
 
     PlanBuilder(final CsrOp.Source source) {
         add(source);
@@ -45,7 +46,7 @@ final class PlanBuilder {
         this.nodes.addAll(other.nodes);
         this.lane = other.lane;
         this.recordsSource = other.recordsSource;
-        this.closed = other.closed;
+        this.hardClosed = other.hardClosed;
     }
 
     PlanBuilder copy() {
@@ -60,16 +61,16 @@ final class PlanBuilder {
         nodes.addAll(copy.nodes);
         lane = copy.lane;
         recordsSource = copy.recordsSource;
-        closed = copy.closed;
+        hardClosed = copy.hardClosed;
     }
 
     /**
      * Appends a node.
      *
-     * @throws Reject if the plan is closed or the node cannot read the 
current lane
+     * @throws Reject if the plan was explicitly closed or the node cannot 
read the current lane
      */
     void add(final CsrOp node) {
-        if (closed) throw new Reject("no step may follow the terminal " + 
nodes.get(nodes.size() - 1).name());
+        if (hardClosed) throw new Reject("the plan cannot continue after a 
side-effect writer");
         try {
             lane = node.outputLane(nodes.isEmpty() ? null : lane);
         } catch (final IllegalArgumentException e) {
@@ -77,7 +78,6 @@ final class PlanBuilder {
         }
         recordsSource = lane == Lane.E && 
node.outputRecordsSource(recordsSource);
         nodes.add(node);
-        if (node instanceof CsrOp.Terminal) closed = true;
     }
 
     /**
@@ -86,7 +86,7 @@ final class PlanBuilder {
     void replace(final int index, final CsrOp node) {
         final List<CsrOp> old = new ArrayList<>(nodes);
         nodes.clear();
-        closed = false;
+        hardClosed = false;
         for (int i = 0; i < old.size(); i++) add(i == index ? node : 
old.get(i));
     }
 
@@ -110,11 +110,11 @@ final class PlanBuilder {
      * Ends the plan after the last node without a terminal.
      */
     void close() {
-        closed = true;
+        hardClosed = true;
     }
 
-    boolean isClosed() {
-        return closed;
+    boolean endsInTerminal() {
+        return nodes.get(nodes.size() - 1) instanceof CsrOp.Terminal;
     }
 
     boolean hasOps() {
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/Planner.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/Planner.java
index 5e0b2ebb55..a74213398c 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/Planner.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/Planner.java
@@ -210,7 +210,7 @@ final class Planner {
                     }
                 }
                 j += used;
-                if (close || b.isClosed()) break;
+                if (close) break;
             } catch (final Reject r) {
                 b.commit(saved);
                 note(steps.get(j), j, r.getMessage());
diff --git 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/StepCompiler.java
 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/StepCompiler.java
index a0cdd4ae99..0408a36465 100644
--- 
a/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/StepCompiler.java
+++ 
b/gremlin-core/src/main/java/org/apache/tinkerpop/gremlin/structure/snapshot/process/strategy/StepCompiler.java
@@ -24,6 +24,7 @@ import org.apache.tinkerpop.gremlin.process.traversal.Pick;
 import org.apache.tinkerpop.gremlin.process.traversal.Step;
 import org.apache.tinkerpop.gremlin.process.traversal.Traversal;
 import 
org.apache.tinkerpop.gremlin.process.traversal.lambda.AbstractLambdaTraversal;
+import org.apache.tinkerpop.gremlin.process.traversal.lambda.ColumnTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.lambda.ConstantTraversal;
 import 
org.apache.tinkerpop.gremlin.process.traversal.lambda.GValueConstantTraversal;
 import org.apache.tinkerpop.gremlin.process.traversal.lambda.IdentityTraversal;
@@ -58,6 +59,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.filter.TraversalFilte
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CoalesceStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.ConstantStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.CountLocalStep;
 import 
org.apache.tinkerpop.gremlin.process.traversal.step.map.EdgeOtherVertexStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.EdgeVertexStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.ElementMapStep;
@@ -71,6 +73,7 @@ import 
org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.LabelsStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.LoopsStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxGlobalStep;
+import org.apache.tinkerpop.gremlin.process.traversal.step.map.MaxLocalStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MeanGlobalStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.MinGlobalStep;
 import org.apache.tinkerpop.gremlin.process.traversal.step.map.NoOpBarrierStep;
@@ -442,6 +445,10 @@ final class StepCompiler {
             b.add(new Terminals.Count());
             return 1;
         }
+        if (s instanceof CountLocalStep) {
+            b.add(new MapOps.CountLocal());
+            return 1;
+        }
         if (s instanceof SumGlobalStep) {
             requireValues(lane, "sum()");
             b.add(new Terminals.Sum());
@@ -457,6 +464,10 @@ final class StepCompiler {
             b.add(new Terminals.Max());
             return 1;
         }
+        if (s instanceof MaxLocalStep) {
+            b.add(new MapOps.MaxLocal());
+            return 1;
+        }
         if (s instanceof MeanGlobalStep) {
             requireValues(lane, "mean()");
             b.add(new Terminals.Mean());
@@ -523,6 +534,10 @@ final class StepCompiler {
         if (lane != Lane.VAL) throw new Reject(what + " needs values, not " + 
lane);
     }
 
+    private static void requireDecoded(final Lane lane, final String what) {
+        if (lane != Lane.VAL && lane != Lane.SCALAR) throw new Reject(what + " 
needs a decoded value, not " + lane);
+    }
+
     private static void requireMapLane(final Lane lane) {
         if (lane != Lane.V && lane != Lane.E) throw new Reject("a map step 
needs vertices or edges, not " + lane);
     }
@@ -756,9 +771,10 @@ final class StepCompiler {
      * wrapper), constants, or a child plan whose first result is the key.
      */
     private Keys.Key key(final Traversal.Admin<?, ?> by, final Lane lane, 
final Scope sc) {
-        if (lane == null || lane == Lane.SCALAR) throw new Reject("a key over 
" + lane);
+        if (lane == null) throw new Reject("a key over " + lane);
         if (by == null || by instanceof IdentityTraversal) return new 
Keys.Identity();
         if (by instanceof TokenTraversal) {
+            if (lane == Lane.SCALAR) throw new Reject("by(token) over " + 
lane);
             final T token = ((TokenTraversal<?, ?>) by).getToken();
             final boolean ok;
             if (token == T.id) ok = lane == Lane.V || lane == Lane.E || lane 
== Lane.VP;
@@ -772,9 +788,10 @@ final class StepCompiler {
             final ValueTraversal<?, ?> vt = (ValueTraversal<?, ?>) by;
             final boolean productive = vt.getBypassTraversal() != null;
             if (productive && !isProductiveBypass(vt)) throw new Reject("an 
unrecognized by() bypass");
+            final String name = vt.getPropertyKey();
+            if (lane == Lane.VAL || lane == Lane.SCALAR) return new 
Keys.Value(-1, name, productive);
             if (lane != Lane.V && lane != Lane.E) throw new Reject("by(key) 
over " + lane);
             requireFull("properties");
-            final String name = vt.getPropertyKey();
             final int code = lane == Lane.V ? graph.vertexKeyCode(name) : 
graph.edgeKeyCode(name);
             if (code < 0) {
                 if (productive) return new Keys.Const(null);
@@ -843,7 +860,7 @@ final class StepCompiler {
      */
     private CsrPlan child(final Traversal.Admin<?, ?> t, final Lane lane, 
final Scope sc, final boolean allowTerminal) {
         if (t instanceof AbstractLambdaTraversal) throw new Reject("a lambda 
traversal child");
-        if (lane == null || lane == Lane.SCALAR) throw new Reject("a child 
over " + lane);
+        if (lane == null) throw new Reject("a child over " + lane);
         final CsrOp.Source input = new Sources.Input(lane);
         checkImplemented(input);
         final PlanBuilder b = new PlanBuilder(input);
@@ -859,7 +876,7 @@ final class StepCompiler {
             if (analysis.anyReferenced(s.getLabels())) throw new Reject("the 
label of " + name(s) + " is read");
             i += compile(steps, i, b, inner);
         }
-        if (b.isClosed() && !allowTerminal) throw new Reject("a reducing 
child");
+        if (b.endsInTerminal() && !allowTerminal) throw new Reject("a reducing 
child");
         return b.build();
     }
 
@@ -874,6 +891,11 @@ final class StepCompiler {
         if (lt.getBypassTraversal() != null) throw new Reject("a map over a 
bypass traversal");
         if (lt instanceof IdentityTraversal) return;
         final Lane lane = b.lane();
+        if (lt instanceof ColumnTraversal) {
+            requireDecoded(lane, "select(column)");
+            b.add(new MapOps.SelectColumn(((ColumnTraversal) lt).getColumn()));
+            return;
+        }
         if (lt instanceof TokenTraversal) {
             final T token = ((TokenTraversal<?, ?>) lt).getToken();
             if (token == T.id) {
@@ -926,7 +948,7 @@ final class StepCompiler {
 
     private void project(final ProjectStep<?, ?> p, final PlanBuilder b, final 
Scope sc) {
         final Lane lane = b.lane();
-        if (lane == null || lane == Lane.SCALAR) throw new Reject("project() 
of " + lane);
+        if (lane == null) throw new Reject("project() of " + lane);
         final List<String> names = p.getProjectKeys();
         final List<? extends Traversal.Admin<?, ?>> ring = 
p.getTraversalRing().getTraversals();
         final List<Keys.Key> keys = new ArrayList<>(names.size());
@@ -1055,7 +1077,7 @@ final class StepCompiler {
             if (analysis.anyReferenced(s.getLabels())) throw new Reject("the 
label of " + name(s) + " is read");
             i += compile(steps, i, b, inner);
         }
-        if (b.isClosed()) throw new Reject("a reducing loop child");
+        if (b.endsInTerminal()) throw new Reject("a reducing loop child");
         return b.build();
     }
 
diff --git a/spark-gremlin/src/test/csr-memory/csrbench.py 
b/spark-gremlin/src/test/csr-memory/csrbench.py
index 2b846410a2..51bd4cc8b2 100755
--- a/spark-gremlin/src/test/csr-memory/csrbench.py
+++ b/spark-gremlin/src/test/csr-memory/csrbench.py
@@ -48,7 +48,7 @@ DEFAULT_QUERY_FILE = os.path.join(
 DEFAULT_CLASSPATH_FILE = os.path.join(REPO_ROOT, "spark-gremlin", "target", 
"csrbench-classpath.txt")
 MB = 1 << 20
 GB = 1 << 30
-CSR_SYSTEMS = ("csr", "csr-native", "csr-facade")
+CSR_SYSTEMS = ("csr", "csr-native", "csr-facade", "csr-computer")
 
 ADD_OPENS = [
     "java.base/java.util.concurrent.atomic", "java.base/java.util", 
"java.base/java.lang",
diff --git 
a/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/csr/CsrMemoryRun.java
 
b/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/csr/CsrMemoryRun.java
index 135f463f17..a65668bbe5 100644
--- 
a/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/csr/CsrMemoryRun.java
+++ 
b/spark-gremlin/src/test/java/org/apache/tinkerpop/gremlin/spark/csr/CsrMemoryRun.java
@@ -41,6 +41,7 @@ import 
org.apache.tinkerpop.gremlin.structure.snapshot.build.HybridSnapshotBuild
 import org.apache.tinkerpop.gremlin.structure.snapshot.build.SnapshotBuilder;
 import 
org.apache.tinkerpop.gremlin.structure.snapshot.build.StreamingSnapshotBuilder;
 import org.apache.tinkerpop.gremlin.structure.snapshot.graph.CsrGraph;
+import org.apache.tinkerpop.gremlin.structure.snapshot.graph.CsrGraphComputer;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.CsrNative;
 import org.apache.tinkerpop.gremlin.structure.snapshot.process.CsrSuperStep;
 import 
org.apache.tinkerpop.gremlin.structure.snapshot.process.exec.CsrMemoryBudgetException;
@@ -82,7 +83,7 @@ import java.util.Set;
  * <pre>
  * java [jvm flags] org.apache.tinkerpop.gremlin.spark.csr.CsrMemoryRun
  *   --mode build|open|query
- *   --system tinkergraph|csr-native|csr-facade|tinkergraph-computer|spark   
(query mode; open mode: tinkergraph|csr)
+ *   --system 
tinkergraph|csr-native|csr-facade|tinkergraph-computer|spark|csr-computer   
(query mode; open mode: tinkergraph|csr)
  *   --dataset &lt;.kryo file&gt;               (required for tinkergraph*, 
spark, and build)
  *   --snapshot &lt;snapshot dir&gt;            (csr-* and open csr; build 
writes here and it must not exist)
  *   --query-file &lt;file&gt; --query &lt;id&gt;     (query mode; the file is 
described below)
@@ -111,7 +112,9 @@ import java.util.Set;
  * uses) and runs OLTP. {@code tinkergraph-computer} is the same graph with
  * {@code g.withComputer()}. {@code csr-native} opens the snapshot and runs 
with {@code csrMemoryBudget} set from
  * {@code --csr-budget} and {@code csrScratchDirectory} under the work 
directory; {@code csr-facade} adds
- * {@code withoutStrategies(CsrNativeStrategy)}. {@code spark} runs {@code 
g.withComputer(SparkGraphComputer)} over a
+ * {@code withoutStrategies(CsrNativeStrategy)}. {@code csr-computer} opens 
the snapshot and runs with
+ * {@code withComputer(CsrGraphComputer)}, which executes the vertex-program 
steps below directly over the snapshot.
+ * {@code spark} runs {@code g.withComputer(SparkGraphComputer)} over a
  * {@link HadoopGraph} that reads the Gryo file with {@code GryoInputFormat} 
in one JVM ({@code spark.master}
  * {@code local[*]} by default, the Kryo serializer with {@code 
GryoRegistrator}, graph and persist storage levels
  * {@code MEMORY_AND_DISK}, {@code spark.local.dir} and the Gremlin output 
location under the work directory, the Spark
@@ -132,8 +135,9 @@ import java.util.Set;
  * <p/>
  * <b>Vertex-program steps.</b> {@code pageRank()}, {@code 
connectedComponent()} and {@code shortestPath()}, with their
  * {@code PageRank}, {@code ConnectedComponent} and {@code ShortestPath} 
{@code with()} options, are part of
- * gremlin-lang, so such queries are ordinary lines that list only {@code 
tinkergraph-computer} and {@code spark}
- * (CSR has no graph computer, and OLTP TinkerGraph rejects the steps); the 
other systems report UNSUPPORTED. The
+ * gremlin-lang, and so is {@code peerPressure()} with its {@code 
PeerPressure} options. Such queries are ordinary
+ * lines that list the graph computers {@code tinkergraph-computer}, {@code 
spark} and {@code csr-computer}; OLTP
+ * TinkerGraph and the OLTP CSR systems reject the steps and report 
UNSUPPORTED. The
  * query reads what the program wrote ({@code values('pageRank')}, {@code 
by('component')}, the paths) in the same
  * traversal, so count and hash cover it. On Spark the steps chain jobs 
through the Gryo graph writer and output
  * location configured below.
@@ -170,11 +174,12 @@ public final class CsrMemoryRun {
     private static final int SAMPLE_RESULTS = 10;
     private static final int SAMPLE_CHARS = 2000;
 
-    private static final Set<String> SYSTEMS = Set.of("tinkergraph", 
"csr-native", "csr-facade", "tinkergraph-computer", "spark");
+    private static final Set<String> SYSTEMS = Set.of("tinkergraph", 
"csr-native", "csr-facade", "tinkergraph-computer", "spark",
+            "csr-computer");
 
     private static final String USAGE = String.join(System.lineSeparator(),
             "Usage: CsrMemoryRun --mode build|open|query [options]",
-            "  --system 
tinkergraph|csr-native|csr-facade|tinkergraph-computer|spark   (open: 
tinkergraph|csr)",
+            "  --system 
tinkergraph|csr-native|csr-facade|tinkergraph-computer|spark|csr-computer   
(open: tinkergraph|csr)",
             "  --dataset <.kryo>   --snapshot <dir>   --query-file <file> 
--query <id>   --warm <n> (3)",
             "  --timeout-seconds <n>   --csr-budget <bytes> (1073741824)   
--builder heap|streaming|hybrid (streaming)",
             "  --source gryo|tinkergraph (gryo)   --builder-budget <bytes> 
(268435456)",
@@ -418,6 +423,10 @@ public final class CsrMemoryRun {
                 openCsr(false);
                 g = 
csrGraph.traversal().withoutStrategies(CsrNativeStrategy.class);
                 break;
+            case "csr-computer":
+                openCsr(false);
+                g = csrGraph.traversal().withComputer(CsrGraphComputer.class);
+                break;
             case "spark":
                 require(config.dataset, "--dataset");
                 hadoopGraph = HadoopGraph.open(sparkConfiguration());
diff --git 
a/spark-gremlin/src/test/resources/org/apache/tinkerpop/gremlin/spark/csr/csr-queries.txt
 
b/spark-gremlin/src/test/resources/org/apache/tinkerpop/gremlin/spark/csr/csr-queries.txt
index 9240ae5261..3a7dc3d873 100644
--- 
a/spark-gremlin/src/test/resources/org/apache/tinkerpop/gremlin/spark/csr/csr-queries.txt
+++ 
b/spark-gremlin/src/test/resources/org/apache/tinkerpop/gremlin/spark/csr/csr-queries.txt
@@ -183,8 +183,11 @@ sid-groupcount-id    | small | * | 
g.V().groupCount().by(id).count(local)
 # 
---------------------------------------------------------------------------------------------------------------------
 # Graph algorithms (alg-*), on the standard power-law graphs. Each algorithm 
has a recipe written in plain Gremlin
 # (docs/src/recipes), which runs on every system, and where TinkerPop has one 
a vertex-program step (pageRank(),
-# connectedComponent(), shortestPath()), which only the graph computers can 
run: the -vp queries list
-# tinkergraph-computer and spark, and the other systems report UNSUPPORTED. 
Both forms end in the same summary (a
+# peerPressure(), connectedComponent(), shortestPath()), which only the graph 
computers can run: the -vp queries list
+# tinkergraph-computer, spark and csr-computer (CsrGraphComputer), and the 
other systems report UNSUPPORTED.
+# matrix-algorithms.json runs only the four vertex programs (pagerank, 
peerpressure, cc, sp) on the three graph
+# computers; the recipes and the other -vp variants stay here for 
matrix-algorithms-recipes.json, which is kept for
+# reference and not run. Both forms end in the same summary (a
 # count, a min, a list of counts, the ids of a path), so that recipe and 
vertex program can be compared by result hash; the exception is
 # alg-eigen-pagerank-vp, a different centrality whose top ten is compared with 
alg-eigen-walks by eye
 # (extra.resultSample).
@@ -212,7 +215,12 @@ alg-degree-top10      | small | * | g.V().project('v', 
'd').by(id).by(bothE().co
 # alg: eigenvector centrality. The recipe counts out-walks of length 0-4 that 
end at each vertex; barrier() bulks the
 # traversers at each step, without it the walks are enumerated one by one. 
pageRank() is the vertex-program reference.
 alg-eigen-walks       | small | * | 
g.V().repeat(groupCount('m').by(id).out().barrier()).times(5).cap('m').order(local).by(values,
 desc).limit(local, 10)
-alg-eigen-pagerank-vp | small | tinkergraph-computer,spark | 
g.V().pageRank().with(PageRank.propertyName, 'pageRank').with(PageRank.times, 
20).order().by('pageRank', desc).by(id).limit(10).id() | ordered
+alg-eigen-pagerank-vp | small | tinkergraph-computer,spark,csr-computer | 
g.V().pageRank().with(PageRank.propertyName, 'pageRank').with(PageRank.times, 
20).order().by('pageRank', desc).by(id).limit(10).id() | ordered
+
+# alg: label propagation clustering (peerPressure(), 20 iterations), 
summarized as cluster count and largest cluster
+# size. It has no recipe. Ties in the vote go to the smaller cluster id by 
string order, so the result is
+# deterministic, but vote sums are floating point and a different summation 
order can still change a close vote.
+alg-peerpressure-vp   | small | tinkergraph-computer,spark,csr-computer | 
g.V().peerPressure().with(PeerPressure.propertyName, 
'cluster').with(PeerPressure.times, 
20).groupCount().by('cluster').select(values).project('clusters', 
'largest').by(count(local)).by(max(local))
 
 # alg: connected components (weak), summarized as component count and largest 
component size. alg-cc-recipe runs a
 # dedup() breadth-first search per vertex not yet seen, inside local(); the 
graph computers reject local() beyond the
@@ -221,37 +229,37 @@ alg-eigen-pagerank-vp | small | 
tinkergraph-computer,spark | g.V().pageRank().wi
 # alg-cc-vp reads the default component key, because gremlin-lang does not 
implement ConnectedComponent.propertyName.
 alg-cc-recipe         | small | * | 
g.V().local(where(without('a')).union(identity(), 
repeat(both().dedup()).emit()).dedup().aggregate('a').count()).is(gt(0)).fold().project('components',
 'largest').by(count(local)).by(max(local))
 alg-cc-recipe-doc     | small | * | g.V().emit(or(cyclicPath(), 
not(both()))).repeat(where(without('a')).local(aggregate('a')).both()).until(cyclicPath()).group().by(path().unfold().limit(1)).by(path().unfold().dedup().fold()).select(values).unfold().count(local).fold().project('components',
 'largest').by(count(local)).by(max(local))
-alg-cc-vp             | small | tinkergraph-computer,spark | 
g.V().connectedComponent().groupCount().by('gremlin.connectedComponentVertexProgram.component').select(values).project('components',
 'largest').by(count(local)).by(max(local))
+alg-cc-vp             | small | tinkergraph-computer,spark,csr-computer | 
g.V().connectedComponent().groupCount().by('gremlin.connectedComponentVertexProgram.component').select(values).project('components',
 'largest').by(count(local)).by(max(local))
 
 # alg: unweighted shortest path src -> sink over out(), as the vertex count of 
the shortest path; the recipe is bounded
 # at 3 hops and shortestPath() at maxDistance 3
 alg-sp-recipe-p12     | small | * | 
g.V(3440L).repeat(out().simplePath()).until(or(hasId(2166L), 
loops().is(3))).hasId(2166L).path().count(local).min()
-alg-sp-vp-p12         | small | tinkergraph-computer,spark | 
g.V(3440L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(2166L)).with(ShortestPath.maxDistance, 3).count(local).min()
+alg-sp-vp-p12         | small | tinkergraph-computer,spark,csr-computer | 
g.V(3440L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(2166L)).with(ShortestPath.maxDistance, 3).count(local).min()
 alg-sp-recipe-p16     | small | * | 
g.V(57232L).repeat(out().simplePath()).until(or(hasId(10358L), 
loops().is(3))).hasId(10358L).path().count(local).min()
-alg-sp-vp-p16         | small | tinkergraph-computer,spark | 
g.V(57232L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.maxDistance, 3).count(local).min()
+alg-sp-vp-p16         | small | tinkergraph-computer,spark,csr-computer | 
g.V(57232L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.maxDistance, 3).count(local).min()
 alg-sp-recipe-p18     | small | * | 
g.V(97549L).repeat(out().simplePath()).until(or(hasId(10358L), 
loops().is(3))).hasId(10358L).path().count(local).min()
-alg-sp-vp-p18         | small | tinkergraph-computer,spark | 
g.V(97549L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.maxDistance, 3).count(local).min()
+alg-sp-vp-p18         | small | tinkergraph-computer,spark,csr-computer | 
g.V(97549L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.maxDistance, 3).count(local).min()
 
 # alg: weighted shortest path wsrc -> sink over out() by the edge weight, as 
the vertex and edge ids of the lightest
 # path; the recipe is bounded at 3 hops, shortestPath() is not (maxDistance 
only bounds hop counts). The distance itself
 # cannot be compared: shortestPath() returns paths of reference elements, 
whose weight cannot be read back on a graph
 # computer. CsrAlgorithmSources prints the distance.
 alg-wsp-recipe-p12    | small | * | 
g.withSack(0.0d).V(3440L).repeat(outE().sack(sum).by('weight').inV().simplePath()).until(or(hasId(2166L),
 
loops().is(3))).hasId(2166L).order().by(sack()).limit(1).path().map(unfold().id().fold())
-alg-wsp-vp-p12        | small | tinkergraph-computer,spark | 
g.V(3440L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(2166L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
+alg-wsp-vp-p12        | small | tinkergraph-computer,spark,csr-computer | 
g.V(3440L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(2166L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
 alg-wsp-recipe-p16    | small | * | 
g.withSack(0.0d).V(525L).repeat(outE().sack(sum).by('weight').inV().simplePath()).until(or(hasId(10358L),
 
loops().is(3))).hasId(10358L).order().by(sack()).limit(1).path().map(unfold().id().fold())
-alg-wsp-vp-p16        | small | tinkergraph-computer,spark | 
g.V(525L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
+alg-wsp-vp-p16        | small | tinkergraph-computer,spark,csr-computer | 
g.V(525L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
 alg-wsp-recipe-p18    | small | * | 
g.withSack(0.0d).V(33986L).repeat(outE().sack(sum).by('weight').inV().simplePath()).until(or(hasId(10358L),
 
loops().is(3))).hasId(10358L).order().by(sack()).limit(1).path().map(unfold().id().fold())
-alg-wsp-vp-p18        | small | tinkergraph-computer,spark | 
g.V(33986L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
+alg-wsp-vp-p18        | small | tinkergraph-computer,spark,csr-computer | 
g.V(33986L).shortestPath().with(ShortestPath.edges, 
Direction.OUT).with(ShortestPath.distance, 'weight').with(ShortestPath.target, 
hasId(10358L)).with(ShortestPath.includeEdges, 
true).limit(1).map(unfold().id().fold())
 
 # alg: single-source both() distances from bsrc, as the vertex counts at 
distance 1..k in distance order. The
 # recipe groups by loops() inside the repeat (dedup() keeps the first, 
shortest, visit); shortestPath() keeps one path
 # per end vertex and groups by path length.
 alg-bfs-recipe-p12    | small | * | 
g.V(1759L).repeat(both().not(hasId(1759L)).dedup().groupCount('m').by(loops())).times(3).cap('m').order(local).by(keys).select(values)
-alg-bfs-vp-p12        | small | tinkergraph-computer,spark | 
g.V(1759L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
+alg-bfs-vp-p12        | small | tinkergraph-computer,spark,csr-computer | 
g.V(1759L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
 alg-bfs-recipe-p16    | small | * | 
g.V(1764L).repeat(both().not(hasId(1764L)).dedup().groupCount('m').by(loops())).times(3).cap('m').order(local).by(keys).select(values)
-alg-bfs-vp-p16        | small | tinkergraph-computer,spark | 
g.V(1764L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
+alg-bfs-vp-p16        | small | tinkergraph-computer,spark,csr-computer | 
g.V(1764L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
 alg-bfs-recipe-p18    | small | * | 
g.V(1660L).repeat(both().not(hasId(1660L)).dedup().groupCount('m').by(loops())).times(3).cap('m').order(local).by(keys).select(values)
-alg-bfs-vp-p18        | small | tinkergraph-computer,spark | 
g.V(1660L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
+alg-bfs-vp-p18        | small | tinkergraph-computer,spark,csr-computer | 
g.V(1660L).shortestPath().with(ShortestPath.maxDistance, 
3).dedup().by(tail(local, 
1)).filter(count(local).is(gt(1))).groupCount().by(count(local)).order(local).by(keys).select(values)
 
 # alg: directed three-cycles, small graph (powerlaw-12) only. The first counts 
closed out-walks of length three
 # (parallel edges and self-loops included); the second is the cycle-detection 
recipe and counts distinct triangles.

Reply via email to