Skip to content

Clarify local scope semantics and add explicit type check in SumLocalStep - #3616

Open
xiazcy wants to merge 1 commit into
3.7-devfrom
sum-local-semantics
Open

Clarify local scope semantics and add explicit type check in SumLocalStep#3616
xiazcy wants to merge 1 commit into
3.7-devfrom
sum-local-semantics

Conversation

@xiazcy

@xiazcy xiazcy commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes a non-deterministic type-safety hole in SumLocalStep and documents the Scope.local boxing semantics in the
provider semantics specification.

Problem

sum(local) on a single non-numeric scalar (e.g. g.inject("hello").sum(local)) would non-deterministically either
throw ClassCastException or pass the value through as identity, depending on JIT compilation state. This is because
the generic bound E extends Number is erased at runtime, and the arithmetic call that would enforce it
(NumberHelper.add) is only reached when there are 2+ elements. mean(local) was already correct (always calls
div()), while min(local)/max(local) are unaffected (they use Comparable bounds, and NumberHelper.min/max
correctly falls back to compareTo for non-Numbers).

Fix

Added an explicit instanceof Number check in SumLocalStep after untilNonNull() returns the first element, making
the error deterministic.

Documentation

Added a "Local scope and single values" section ([[gremlin-semantics-local-scope-boxing]]) to
gremlin-semantics.asciidoc specifying:

  • The wrapping dispatch (LIST/SET/array/MAP iterate directly; anything else wraps to a single-element sequence)
  • Scalar numeric identity behavior (sum(local) on 2929)
  • Type error contract for incompatible input (regardless of collection size)

Tests

  • Gherkin (Sum.feature): sum(local) on a non-numeric list (error) and a single non-numeric scalar (error),
    with GLV translations for .NET, Go, JS, Python
  • Unit (SumLocalStepTest): numeric scalar identity + non-numeric error
  • Unit (MeanLocalStepTest): numeric scalar identity + non-numeric error
  • Unit (MinLocalStepTest): numeric identity, String identity (valid Comparable), String-list min, numeric-list
    min
  • Unit (MaxLocalStepTest): numeric identity, String identity, String-list max, numeric-list max
  • Unit (IteratorUtilsTest): Number singleton wrapping + generic Object singleton wrapping

VOTE +1

… boxing semantics

Add an instanceof Number guard in SumLocalStep to deterministically reject non-numeric input on the single-element path, fixing a JIT-dependent type-erasure hole where sum(local) on a non-numeric scalar would sometimes pass through as identity instead of erroring.

Document the Scope.local boxing/wrapping contract in the semantics doc (scalars are coerced to single-element sequences) and add tests locking the behavior for sum, mean, min, and max local steps, plus the IteratorUtils singleton-wrapping contract.

Assisted-by: Kiro:claude-opus-4 [kiro-cli]
@kenhuuu

kenhuuu commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

I'm not sure this is the right thing to do. This changes 3.7-dev semantics to make them slightly more in line with other local steps, but if you take into account all steps then its just a similar behavior quirk that you see in other steps. There are steps that take predicates like is(), has(), etc. that will silently filter out incorrect types rather than throw. There has been an ongoing debate about whether a step should do this filtering or throw on incorrect type and this is just an extension of that argument. So until we sort that out, I wouldn't make this change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants