@@ -189,11 +189,156 @@ class Evaluator(
189189 visitExpr(e.returned)(s)
190190 }
191191
192- def visitComp (e : Comp )(implicit scope : ValScope ): Val =
193- Val .Arr (
194- e.pos,
195- visitComp(e.first :: e.rest.toList, Array (scope)).map(s => visitAsLazy(e.value)(s))
196- )
192+ def visitComp (e : Comp )(implicit scope : ValScope ): Val = {
193+ val results = new collection.mutable.ArrayBuilder .ofRef[Eval ]
194+ results.sizeHint(16 )
195+ visitCompFused(e.first :: e.rest.toList, scope, e.value, results)
196+ Val .Arr (e.pos, results.result())
197+ }
198+
199+ /**
200+ * Fused scope-building + body evaluation: eliminates intermediate scope array allocation. Instead
201+ * of first collecting all valid scopes into an Array[ValScope] and then mapping over them with
202+ * visitAsLazy, this method directly appends body results as it encounters valid scopes. For
203+ * nested comprehensions like `[x+y for x in arr for y in arr if x==y]`, this avoids allocating
204+ * O(n²) intermediate scopes — only the O(n) matching results are materialized.
205+ *
206+ * For innermost ForSpec with BinaryOp(ValidId,ValidId) body, inlines scope lookups and numeric
207+ * binary-op dispatch to avoid 3× visitExpr overhead per iteration.
208+ */
209+ private def visitCompFused (
210+ specs : List [CompSpec ],
211+ scope : ValScope ,
212+ body : Expr ,
213+ results : collection.mutable.ArrayBuilder .ofRef[Eval ]
214+ ): Unit = specs match {
215+ case (spec @ ForSpec (_, name, expr)) :: rest =>
216+ visitExpr(expr)(scope) match {
217+ case a : Val .Arr =>
218+ if (debugStats != null ) debugStats.arrayCompIterations += a.length
219+ val lazyArr = a.asLazyArray
220+ if (rest.isEmpty) {
221+ // Innermost loop: try BinaryOp(ValidId,ValidId) fast path
222+ body match {
223+ case binOp : BinaryOp
224+ if binOp.lhs.tag == ExprTags .ValidId
225+ && binOp.rhs.tag == ExprTags .ValidId =>
226+ // Fast path: reuse mutable scope, inline scope lookups + binary-op dispatch.
227+ // NOTE: Evaluates eagerly (not lazy). Both go-jsonnet and jrsonnet also
228+ // evaluate comprehensions eagerly, so this is compatible. Eagerness is
229+ // required for mutable scope reuse — a lazy thunk would capture the
230+ // mutable scope and see stale bindings from later iterations.
231+ val mutableScope = scope.extendBy(1 )
232+ val slot = scope.bindings.length
233+ val bindings = mutableScope.bindings
234+ val lhsIdx = binOp.lhs.asInstanceOf [ValidId ].nameIdx
235+ val rhsIdx = binOp.rhs.asInstanceOf [ValidId ].nameIdx
236+ val op = binOp.op
237+ val bpos = binOp.pos
238+ var j = 0
239+ while (j < lazyArr.length) {
240+ bindings(slot) = lazyArr(j)
241+ val l = bindings(lhsIdx).value
242+ val r = bindings(rhsIdx).value
243+ (l, r) match {
244+ // Only dispatch to numeric fast path for ops it handles (0-16 except OP_in=11).
245+ // OP_in expects string+object, OP_&&/OP_|| need short-circuit semantics.
246+ case (ln : Val .Num , rn : Val .Num )
247+ if op <= Expr .BinaryOp .OP_| && op != Expr .BinaryOp .OP_in =>
248+ results += evalBinaryOpNumNum(op, ln, rn, bpos)
249+ case _ =>
250+ // Fallback to general evaluator for non-numeric types
251+ results += visitExpr(binOp)(mutableScope)
252+ }
253+ j += 1
254+ }
255+ case _ =>
256+ var j = 0
257+ while (j < lazyArr.length) {
258+ results += visitAsLazy(body)(scope.extendSimple(lazyArr(j)))
259+ j += 1
260+ }
261+ }
262+ } else {
263+ // Outer loop: recurse for remaining specs
264+ var j = 0
265+ while (j < lazyArr.length) {
266+ visitCompFused(rest, scope.extendSimple(lazyArr(j)), body, results)
267+ j += 1
268+ }
269+ }
270+ case r =>
271+ Error .fail(
272+ " In comprehension, can only iterate over array, not " + r.prettyName,
273+ spec
274+ )
275+ }
276+ case (spec @ IfSpec (offset, expr)) :: rest =>
277+ visitExpr(expr)(scope) match {
278+ case Val .True (_) =>
279+ if (rest.isEmpty) results += visitAsLazy(body)(scope)
280+ else visitCompFused(rest, scope, body, results)
281+ case Val .False (_) => // filtered out
282+ case other =>
283+ Error .fail(
284+ " Condition must be boolean, got " + other.prettyName,
285+ spec
286+ )
287+ }
288+ case Nil =>
289+ results += visitAsLazy(body)(scope)
290+ }
291+
292+ /**
293+ * Fast-path binary op evaluation for Num×Num operands within comprehension inner loops. Handles
294+ * the most common operations without visitExpr dispatch overhead.
295+ */
296+ @ inline private def evalBinaryOpNumNum (op : Int , ln : Val .Num , rn : Val .Num , pos : Position ): Val = {
297+ val ld = ln.asDouble
298+ val rd = rn.asDouble
299+ (op : @ switch) match {
300+ case Expr .BinaryOp .OP_+ => Val .Num (pos, ld + rd)
301+ case Expr .BinaryOp .OP_- =>
302+ val r = ld - rd
303+ if (r.isInfinite) Error .fail(" overflow" , pos)
304+ Val .Num (pos, r)
305+ case Expr .BinaryOp .OP_* =>
306+ val r = ld * rd
307+ if (r.isInfinite) Error .fail(" overflow" , pos)
308+ Val .Num (pos, r)
309+ case Expr .BinaryOp .OP_/ =>
310+ if (rd == 0 ) Error .fail(" division by zero" , pos)
311+ val r = ld / rd
312+ if (r.isInfinite) Error .fail(" overflow" , pos)
313+ Val .Num (pos, r)
314+ case Expr .BinaryOp .OP_% => Val .Num (pos, ld % rd)
315+ case Expr .BinaryOp .OP_< => Val .bool(pos, ld < rd)
316+ case Expr .BinaryOp .OP_> => Val .bool(pos, ld > rd)
317+ case Expr .BinaryOp .OP_<= => Val .bool(pos, ld <= rd)
318+ case Expr .BinaryOp .OP_>= => Val .bool(pos, ld >= rd)
319+ case Expr .BinaryOp .OP_== => Val .bool(pos, ld == rd)
320+ case Expr .BinaryOp .OP_!= => Val .bool(pos, ld != rd)
321+ case Expr .BinaryOp .OP_<< =>
322+ val ll = ld.toSafeLong(pos); val rr = rd.toSafeLong(pos)
323+ if (rr < 0 ) Error .fail(" shift by negative exponent" , pos)
324+ if (rr >= 1 && math.abs(ll) >= (1L << (63 - rr)))
325+ Error .fail(" numeric value outside safe integer range for bitwise operation" , pos)
326+ Val .Num (pos, (ll << rr).toDouble)
327+ case Expr .BinaryOp .OP_>> =>
328+ val ll = ld.toSafeLong(pos); val rr = rd.toSafeLong(pos)
329+ if (rr < 0 ) Error .fail(" shift by negative exponent" , pos)
330+ Val .Num (pos, (ll >> rr).toDouble)
331+ case Expr .BinaryOp .OP_& =>
332+ Val .Num (pos, (ld.toSafeLong(pos) & rd.toSafeLong(pos)).toDouble)
333+ case Expr .BinaryOp .OP_^ =>
334+ Val .Num (pos, (ld.toSafeLong(pos) ^ rd.toSafeLong(pos)).toDouble)
335+ case Expr .BinaryOp .OP_| =>
336+ Val .Num (pos, (ld.toSafeLong(pos) | rd.toSafeLong(pos)).toDouble)
337+ case _ =>
338+ // Should be unreachable: caller filters to ops 0-16 except OP_in
339+ throw new AssertionError (s " Unexpected numeric binary op: $op" )
340+ }
341+ }
197342
198343 def visitArr (e : Arr )(implicit scope : ValScope ): Val =
199344 Val .Arr (e.pos, e.value.map(visitAsLazy))
0 commit comments