diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index fd02cb4f..ab077d56 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -2012,8 +2012,10 @@ class Evaluator( case x: Val.Arr => y match { case y: Val.Arr => + if (x eq y) return true val xlen = x.length if (xlen != y.length) return false + if (x.rangeEquals(y)) return true // Skip shared ConcatView prefix — elements are reference-identical var i = x.sharedConcatPrefixLength(y) while (i < xlen) { diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index c717c93c..526a79f2 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -647,6 +647,27 @@ object Val { true } + /** + * If this array is known to be sorted in ascending numeric order (e.g. a forward range), return + * it directly. If it is a reversed range, return the equivalent forward range in O(1). Returns + * null if sort order is unknown and a full sort is needed. + */ + private[sjsonnet] def asSortedIfKnown(newPos: Position): Arr = this match { + case _: RangeArr => if (_reversed) reversed(newPos) else this + case _ => null + } + + /** + * O(1) equality check for range arrays. Two ranges are equal if they describe the same integer + * sequence (same start, length, and direction). Returns false if either array is not a range. + */ + private[sjsonnet] def rangeEquals(other: Arr): Boolean = + (this, other) match { + case (r1: RangeArr, r2: RangeArr) => + r1._length == r2._length && r1._reversed == r2._reversed && r1.rangeFrom == r2.rangeFrom + case _ => false + } + /** * Create a reversed view of this array without copying. The returned Arr shares the same * backing array but flips the reversed flag, so element access is O(1) with zero allocation. @@ -671,7 +692,7 @@ object Val { * * Inspired by jrsonnet's RangeArray (arr/spec.rs). */ - final class RangeArr(pos0: Position, private val rangeFrom: Int, size: Int) + final class RangeArr(pos0: Position, private[sjsonnet] val rangeFrom: Int, size: Int) extends Arr(pos0, null) { _length = size diff --git a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala index e78b9fe7..be91c758 100644 --- a/sjsonnet/src/sjsonnet/stdlib/SetModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/SetModule.scala @@ -82,7 +82,21 @@ object SetModule extends AbstractFunctionModule { Val.Arr(pos, out.result()) } - private def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { + private def sortArr(pos: Position, ev: EvalScope, arr: Val, keyF: Val): Val = { + // Fast path: range arrays are already sorted ascending by construction. + // Avoids O(n) materialization + O(n log n) sort for already-sorted data. + if (keyF == null || keyF.isInstanceOf[Val.False]) { + arr match { + case a: Val.Arr => + val sorted = a.asSortedIfKnown(pos) + if (sorted != null) return sorted + case _ => + } + } + sortArrSlow(pos, ev, arr, keyF) + } + + private def sortArrSlow(pos: Position, ev: EvalScope, arr: Val, keyF: Val) = { val vs = toArrOrString(arr, pos, ev) if (vs.length <= 1) { arr