Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,10 @@ class Evaluator(
def visitUnaryOp(e: UnaryOp)(implicit scope: ValScope): Val = {
val pos = e.pos
(e.op: @switch) match {
case Expr.UnaryOp.OP_+ => Val.Num(pos, visitExprAsDouble(e.value))
case Expr.UnaryOp.OP_- => Val.Num(pos, -visitExprAsDouble(e.value))
case Expr.UnaryOp.OP_~ => Val.Num(pos, (~visitExprAsDouble(e.value).toSafeLong(pos)).toDouble)
case Expr.UnaryOp.OP_+ => Val.cachedNum(pos, visitExprAsDouble(e.value))
case Expr.UnaryOp.OP_- => Val.cachedNum(pos, -visitExprAsDouble(e.value))
case Expr.UnaryOp.OP_~ =>
Val.cachedNum(pos, (~visitExprAsDouble(e.value).toSafeLong(pos)).toDouble)
case Expr.UnaryOp.OP_! =>
visitExpr(e.value) match {
case Val.True(_) => Val.False(pos)
Expand Down Expand Up @@ -683,24 +684,24 @@ class Evaluator(
case Expr.BinaryOp.OP_* =>
val r = visitExprAsDouble(e.lhs) * visitExprAsDouble(e.rhs)
if (r.isInfinite) Error.fail("overflow", pos)
Val.Num(pos, r)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_- =>
val r = visitExprAsDouble(e.lhs) - visitExprAsDouble(e.rhs)
if (r.isInfinite) Error.fail("overflow", pos)
Val.Num(pos, r)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_/ =>
val l = visitExprAsDouble(e.lhs)
val r = visitExprAsDouble(e.rhs)
if (r == 0) Error.fail("division by zero", pos)
val result = l / r
if (result.isInfinite) Error.fail("overflow", pos)
Val.Num(pos, result)
Val.cachedNum(pos, result)
// Polymorphic ops: need visitExpr for type dispatch
case Expr.BinaryOp.OP_% =>
val l = visitExpr(e.lhs)
val r = visitExpr(e.rhs)
(l, r) match {
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l % r)
case (Val.Num(_, l), Val.Num(_, r)) => Val.cachedNum(pos, l % r)
case (Val.Str(_, l), r) => Val.Str(pos, Format.format(l, r, pos))
case _ => failBinOp(l, e.op, r, pos)
}
Expand All @@ -709,7 +710,7 @@ class Evaluator(
val l = visitExpr(e.lhs)
val r = visitExpr(e.rhs)
(l, r) match {
case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l + r)
case (Val.Num(_, l), Val.Num(_, r)) => Val.cachedNum(pos, l + r)
case (Val.Str(_, l), Val.Str(_, r)) => Val.Str(pos, l + r)
case (Val.Str(_, l), r) => Val.Str(pos, l + Materializer.stringify(r))
case (l, Val.Str(_, r)) => Val.Str(pos, Materializer.stringify(l) + r)
Expand All @@ -726,13 +727,13 @@ class Evaluator(
if (rr >= 1 && math.abs(ll) >= (1L << (63 - rr)))
Error.fail("numeric value outside safe integer range for bitwise operation", pos)
else
Val.Num(pos, (ll << rr).toDouble)
Val.cachedNum(pos, (ll << rr).toDouble)

case Expr.BinaryOp.OP_>> =>
val ll = visitExprAsDouble(e.lhs).toSafeLong(pos)
val rr = visitExprAsDouble(e.rhs).toSafeLong(pos)
if (rr < 0) Error.fail("shift by negative exponent", pos)
Val.Num(pos, (ll >> rr).toDouble)
Val.cachedNum(pos, (ll >> rr).toDouble)

// Comparison ops: polymorphic (Num/Str/Arr)
case Expr.BinaryOp.OP_< =>
Expand Down Expand Up @@ -804,21 +805,21 @@ class Evaluator(

// Bitwise ops: pure numeric with safe-integer range check
case Expr.BinaryOp.OP_& =>
Val.Num(
Val.cachedNum(
pos,
(visitExprAsDouble(e.lhs).toSafeLong(pos) &
visitExprAsDouble(e.rhs).toSafeLong(pos)).toDouble
)

case Expr.BinaryOp.OP_^ =>
Val.Num(
Val.cachedNum(
pos,
(visitExprAsDouble(e.lhs).toSafeLong(pos) ^
visitExprAsDouble(e.rhs).toSafeLong(pos)).toDouble
)

case Expr.BinaryOp.OP_| =>
Val.Num(
Val.cachedNum(
pos,
(visitExprAsDouble(e.lhs).toSafeLong(pos) |
visitExprAsDouble(e.rhs).toSafeLong(pos)).toDouble
Expand Down
2 changes: 1 addition & 1 deletion sjsonnet/src/sjsonnet/StaticOptimizer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ class StaticOptimizer(
}
case Expr.UnaryOp.OP_+ =>
v match {
case n: Val.Num => n.pos = pos; n.asInstanceOf[Expr]
case n: Val.Num => Val.Num(pos, n.asDouble)
case _ => fallback
}
case _ => fallback
Expand Down
27 changes: 27 additions & 0 deletions sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,33 @@ object Val {

def bool(pos: Position, b: Boolean): Bool = if (b) True(pos) else False(pos)

/**
* Pre-allocated pool of Val.Num for small non-negative integers 0–255. Used by Evaluator
* arithmetic fast paths to avoid per-operation allocation. Position is synthetic — acceptable for
* intermediate runtime results.
*/
private val numCacheSize = 256
private val numCache: Array[Num] = {
val pos = Position(null, -1)
val arr = new Array[Num](numCacheSize)
var i = 0
while (i < numCacheSize) {
arr(i) = Num(pos, i.toDouble)
i += 1
}
arr
}

/**
* Returns a cached Val.Num for small non-negative integers (0–255), or a fresh instance
* otherwise. Use in evaluator arithmetic paths where the pos is not critical for error reporting.
*/
def cachedNum(pos: Position, d: Double): Num = {
val i = d.toInt
if (i >= 0 && i < numCacheSize && i.toDouble == d) numCache(i)
else Num(pos, d)
}

final case class True(var pos: Position) extends Bool {
def prettyName = "boolean"
}
Expand Down
9 changes: 8 additions & 1 deletion sjsonnet/src/sjsonnet/stdlib/EncodingModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,14 @@ object EncodingModule extends AbstractFunctionModule {
},
builtin("base64DecodeBytes", "str") { (pos, _, str: String) =>
try {
Val.Arr(pos, Base64.getDecoder.decode(str).map(i => Val.Num(pos, i)))
val decoded = Base64.getDecoder.decode(str)
val result = new Array[Eval](decoded.length)
var i = 0
while (i < decoded.length) {
result(i) = Val.cachedNum(pos, (decoded(i) & 0xff).toDouble)
i += 1
}
Val.Arr(pos, result)
} catch {
case e: IllegalArgumentException =>
Error.fail("Invalid base64 string: " + e.getMessage)
Expand Down