Skip to content
Closed
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
133 changes: 99 additions & 34 deletions sjsonnet/src-jvm/sjsonnet/Platform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import com.google.re2j.Pattern
import net.jpountz.xxhash.{StreamingXXHash64, XXHashFactory}
import org.tukaani.xz.LZMA2Options
import org.tukaani.xz.XZOutputStream
import org.yaml.snakeyaml.{LoaderOptions, Yaml}
import org.yaml.snakeyaml.constructor.SafeConstructor
import org.yaml.snakeyaml.{DumperOptions, LoaderOptions, Yaml}
import org.yaml.snakeyaml.nodes.{MappingNode, Node, ScalarNode, SequenceNode, Tag}

import scala.annotation.nowarn
import scala.collection.compat.*
Expand Down Expand Up @@ -73,48 +73,113 @@ object Platform {
xzBytes(s.getBytes(UTF_8), compressionLevel)
}

private def nodeToJson(node: Any): ujson.Value = node match {
case m: java.util.List[?] =>
val buf = new mutable.ArrayBuffer[ujson.Value](m.size)
for (n <- m.asScala) {
buf += nodeToJson(n)
private val Yaml12OctalPattern = java.util.regex.Pattern.compile("[-+]?0o[0-7]+")

private def yamlNodeToJson(node: Node): ujson.Value = node match {
case sn: ScalarNode =>
val value = sn.getValue
val tag = sn.getTag
val isPlain = sn.getScalarStyle == DumperOptions.ScalarStyle.PLAIN

if (isPlain && Yaml12OctalPattern.matcher(value).matches()) {
val negative = value.charAt(0) == '-'
val octalPart =
if (negative || value.charAt(0) == '+') value.substring(3) else value.substring(2)
val result = java.lang.Long.parseUnsignedLong(octalPart, 8)
val signed = if (negative) -result else result
ujson.Num(signed.toDouble)
} else if (tag == Tag.INT) {
val cleaned = value.replace("_", "")
val result: Long =
if (cleaned.startsWith("0x") || cleaned.startsWith("-0x") || cleaned.startsWith("+0x")) {
val negative = cleaned.startsWith("-")
val hex =
if (negative || cleaned.startsWith("+")) cleaned.substring(3)
else cleaned.substring(2)
val v = java.lang.Long.parseUnsignedLong(hex, 16)
if (negative) -v else v
} else if (
cleaned.startsWith("0b") || cleaned.startsWith("-0b") || cleaned.startsWith("+0b")
) {
val negative = cleaned.startsWith("-")
val bin =
if (negative || cleaned.startsWith("+")) cleaned.substring(3)
else cleaned.substring(2)
val v = java.lang.Long.parseUnsignedLong(bin, 2)
if (negative) -v else v
} else if (cleaned.length > 1 && cleaned.startsWith("0") && !cleaned.contains(".")) {
val negative = cleaned.startsWith("-")
val oct = if (negative || cleaned.startsWith("+")) cleaned.substring(1) else cleaned
val v = java.lang.Long.parseUnsignedLong(oct, 8)
if (negative) -v else v
} else if (cleaned.contains(":")) {
val parts = cleaned.split(":")
parts.foldLeft(0L)((acc, p) => acc * 60 + p.trim.toLong)
} else {
cleaned.toLong
}
ujson.Num(result.toDouble)
} else if (tag == Tag.FLOAT) {
val cleaned = value.replace("_", "")
val result = cleaned match {
case ".inf" | ".Inf" | ".INF" => Double.PositiveInfinity
case "-.inf" | "-.Inf" | "-.INF" => Double.NegativeInfinity
case ".nan" | ".NaN" | ".NAN" => Double.NaN
case s if s.contains(":") =>
s.split(":").foldLeft(0.0)((acc, p) => acc * 60 + p.trim.toDouble)
case s => s.toDouble
}
ujson.Num(result)
} else if (tag == Tag.BOOL) {
ujson.Bool(value.toLowerCase match {
case "true" | "yes" | "on" => true
case "false" | "no" | "off" => false
case _ => Error.fail("Invalid YAML boolean: " + value)
})
} else if (tag == Tag.NULL) {
ujson.Null
} else {
ujson.Str(value)
}
ujson.Arr(buf)
case m: java.util.Map[?, ?] =>

case mn: MappingNode =>
val buf = upickle.core.LinkedHashMap[String, ujson.Value]()
buf.sizeHint(m.size)
for ((key, value) <- m.asScala) {
key match {
case k: String => buf(k) = nodeToJson(value)
case _ => Error.fail("Invalid YAML mapping key class: " + key.getClass.getSimpleName)
buf.sizeHint(mn.getValue.size)
for (tuple <- mn.getValue.asScala) {
val key = tuple.getKeyNode match {
case sn: ScalarNode => sn.getValue
case other => Error.fail("Invalid YAML mapping key type: " + other.getTag)
}
buf(key) = yamlNodeToJson(tuple.getValueNode)
}
ujson.Obj(buf)
case null => ujson.Null
case v: String => ujson.Str(v)
case v: Boolean => ujson.Bool(v)
case v: Int => ujson.Num(v.toDouble)
case v: Long => ujson.Num(v.toDouble)
case v: Double => ujson.Num(v)
case v: Float => ujson.Num(v.toDouble)
case v: BigDecimal => ujson.Num(v.toDouble)
case v: BigInt => ujson.Num(v.toDouble)
case v: Short => ujson.Num(v.toDouble)
case _ =>

case sn: SequenceNode =>
val buf = new mutable.ArrayBuffer[ujson.Value](sn.getValue.size)
for (n <- sn.getValue.asScala) {
buf += yamlNodeToJson(n)
}
ujson.Arr(buf)

case _ =>
Error.fail("Unsupported YAML node type: " + node.getClass.getSimpleName)
}

private val YamlDocStartPattern =
java.util.regex.Pattern.compile("\\A\\s*---(?:[ \\t\\n\\r]|\\z)")

def yamlToJson(yamlString: String): ujson.Value = {
try {
val yaml =
new Yaml(new SafeConstructor(new LoaderOptions())).loadAll(yamlString).asScala.toSeq
yaml.size match {
case 0 => ujson.Null
case 1 => nodeToJson(yaml.head)
case _ =>
val buf = new mutable.ArrayBuffer[ujson.Value](yaml.size)
for (doc <- yaml) {
buf += nodeToJson(doc)
val yaml = new Yaml(new LoaderOptions())
val docs = yaml.composeAll(new java.io.StringReader(yamlString)).asScala.toSeq
val hasExplicitDocStart = YamlDocStartPattern.matcher(yamlString).find()
docs.size match {
case 0 => ujson.Null
case 1 if !hasExplicitDocStart => yamlNodeToJson(docs.head)
case _ =>
val buf = new mutable.ArrayBuffer[ujson.Value](docs.size)
for (doc <- docs) {
buf += yamlNodeToJson(doc)
}
ujson.Arr(buf)
}
Expand Down
45 changes: 34 additions & 11 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,9 @@ class Evaluator(
val idx = v.nameIdx
if (idx < scope.length) {
val binding = scope.bindings(idx)
if (binding != null) binding.value match {
if (binding != null) binding match {
case n: Val.Num => n.rawDouble
case _: Val => Double.NaN
case _ => Double.NaN
}
else Double.NaN
Expand All @@ -240,17 +241,17 @@ class Evaluator(
@inline private def tryInlineArith(op: Int, ld: Double, rd: Double, pos: Position): Val =
(op: @switch) match {
case Expr.BinaryOp.OP_* =>
val r = ld * rd; if (r.isInfinite) null else Val.cachedNum(pos, r)
val r = ld * rd; if (r.isNaN || r.isInfinite) null else Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_/ =>
if (rd == 0) null
else { val r = ld / rd; if (r.isNaN || r.isInfinite) null else Val.cachedNum(pos, r) }
case Expr.BinaryOp.OP_% =>
if (rd == 0) null
else { val r = ld % rd; if (r.isNaN) null else Val.cachedNum(pos, r) }
case Expr.BinaryOp.OP_+ =>
val r = ld + rd; if (r.isInfinite) null else Val.cachedNum(pos, r)
val r = ld + rd; if (r.isNaN || r.isInfinite) null else Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_- =>
val r = ld - rd; if (r.isInfinite) null else Val.cachedNum(pos, r)
val r = ld - rd; if (r.isNaN || r.isInfinite) null else Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_<< =>
val ll = ld.toLong; val rl = rd.toLong
if (ll.toDouble != ld || rl.toDouble != rd) null // not safe integers
Expand Down Expand Up @@ -706,23 +707,32 @@ class Evaluator(
val ld = ln.asDouble
val rd = rn.asDouble
(op: @switch) match {
case Expr.BinaryOp.OP_+ => Val.cachedNum(pos, ld + rd)
case Expr.BinaryOp.OP_+ =>
val r = ld + rd
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_- =>
val r = ld - rd
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_* =>
val r = ld * rd
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_/ =>
if (rd == 0) Error.fail("Division by zero.", pos)
val r = ld / rd
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_% =>
if (rd == 0) Error.fail("Division by zero.", pos)
Val.cachedNum(pos, ld % rd)
val r = ld % rd
if (r.isNaN) Error.fail("not a number", pos)
Val.cachedNum(pos, r)
// Use position-free static singletons for boolean results — this method is only called
// from comprehension fast paths where position info on boolean results is unnecessary.
// Avoids 1 object allocation per comparison in inner loops (significant for 1M+ iterations).
Expand Down Expand Up @@ -857,23 +867,28 @@ class Evaluator(
(e.op: @switch) match {
case Expr.BinaryOp.OP_* =>
val r = visitExprAsDouble(e.lhs) * visitExprAsDouble(e.rhs)
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", 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.isNaN) Error.fail("not a number", pos)
if (result.isInfinite) Error.fail("overflow", pos); result
case Expr.BinaryOp.OP_% =>
val l = visitExprAsDouble(e.lhs)
val r = visitExprAsDouble(e.rhs)
if (r == 0) Error.fail("Division by zero.", pos)
l % r
val result = l % r
if (result.isNaN) Error.fail("not a number", pos); result
case Expr.BinaryOp.OP_+ =>
val r = visitExprAsDouble(e.lhs) + visitExprAsDouble(e.rhs)
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos); r
case Expr.BinaryOp.OP_- =>
val r = visitExprAsDouble(e.lhs) - visitExprAsDouble(e.rhs)
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos); r
case Expr.BinaryOp.OP_<< =>
val ll = visitExprAsDouble(e.lhs).toSafeLong(pos)
Expand Down Expand Up @@ -1334,10 +1349,12 @@ class Evaluator(
// Pure numeric fast path: avoid intermediate Val.Num allocation
case Expr.BinaryOp.OP_* =>
val r = visitExprAsDouble(e.lhs) * visitExprAsDouble(e.rhs)
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_- =>
val r = visitExprAsDouble(e.lhs) - visitExprAsDouble(e.rhs)
if (r.isNaN) Error.fail("not a number", pos)
if (r.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, r)
case Expr.BinaryOp.OP_/ =>
Expand Down Expand Up @@ -1370,9 +1387,13 @@ class Evaluator(
val l = visitExpr(e.lhs)
val r = visitExpr(e.rhs)
(l, r) match {
case (Val.Num(_, l), Val.Num(_, r)) => Val.cachedNum(pos, l + r)
case (l: Val.Str, r: Val.Str) => Val.Str.concat(pos, l, r)
case (n: Val.Num, r: Val.Str) =>
case (Val.Num(_, l), Val.Num(_, r)) =>
val result = l + r
if (result.isNaN) Error.fail("not a number", pos)
if (result.isInfinite) Error.fail("overflow", pos)
Val.cachedNum(pos, result)
case (l: Val.Str, r: Val.Str) => Val.Str.concat(pos, l, r)
case (n: Val.Num, r: Val.Str) =>
Val.Str.concat(pos, Val.Str(pos, RenderUtils.renderDouble(n.asDouble)), r)
case (l: Val.Str, n: Val.Num) =>
Val.Str.concat(pos, l, Val.Str(pos, RenderUtils.renderDouble(n.asDouble)))
Expand Down Expand Up @@ -1916,6 +1937,7 @@ class Evaluator(
case Val.Str(_, k) =>
val member = new ObjCompMember(
e.plus,
e.visibility,
this,
binds,
s,
Expand Down Expand Up @@ -2240,11 +2262,12 @@ private[sjsonnet] final class ObjectScopeFactory(
*/
private[sjsonnet] final class ObjCompMember(
plus0: Boolean,
visibility0: Visibility,
private val evaluator: Evaluator,
private val binds: Array[Expr.Bind],
private val compScope: ValScope,
private val valueExpr: Expr)
extends Val.Obj.Member(plus0, Visibility.Normal, deprecatedSkipAsserts = true) {
extends Val.Obj.Member(plus0, visibility0, deprecatedSkipAsserts = true) {
def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
evaluator.checkStackDepth(valueExpr.pos, "object comprehension")
try {
Expand Down
1 change: 1 addition & 0 deletions sjsonnet/src/sjsonnet/Expr.scala
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ object Expr {
key: Expr,
value: Expr,
plus: Boolean, // see https://jsonnet.org/ref/language.html#nested-field-inheritance
visibility: Member.Visibility,
postLocals: Array[Bind],
first: ForSpec,
rest: List[CompSpec])
Expand Down
4 changes: 2 additions & 2 deletions sjsonnet/src/sjsonnet/ExprTransform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,15 @@ abstract class ExprTransform {
if ((x2 eq x) && (y2 eq y)) expr
else ObjExtend(superPos, x2, y2.asInstanceOf[ObjBody])

case ObjBody.ObjComp(pos, p, k, v, pl, o, f, r) =>
case ObjBody.ObjComp(pos, p, k, v, pl, vis, o, f, r) =>
val p2 = transformBinds(p)
val k2 = transform(k)
val v2 = transform(v)
val o2 = transformBinds(o)
val f2 = transform(f).asInstanceOf[ForSpec]
val r2 = transformList(r).asInstanceOf[List[CompSpec]]
if ((p2 eq p) && (k2 eq k) && (v2 eq v) && (o2 eq o) && (f2 eq f) && (r2 eq r)) expr
else ObjBody.ObjComp(pos, p2, k2, v2, pl, o2, f2, r2)
else ObjBody.ObjComp(pos, p2, k2, v2, pl, vis, o2, f2, r2)

case Slice(pos, v, x, y, z) =>
val v2 = transform(v)
Expand Down
3 changes: 2 additions & 1 deletion sjsonnet/src/sjsonnet/Parser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ class Parser(
Expr.FieldName.Dyn(lhs),
plus,
args,
Visibility.Normal,
visibility,
rhsBody
) =>
val rhs = if (args == null) {
Expand Down Expand Up @@ -920,6 +920,7 @@ class Parser(
lhs,
rhs,
plus,
visibility,
postLocals.toArray,
comps._1,
comps._2.toList
Expand Down
2 changes: 1 addition & 1 deletion sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ object PrettyYamlRenderer {
val splits = YamlRenderer.newlinePattern.split(str, -1)
val blockOffsetNumeral = if (str.charAt(0) != ' ') "" else indent
val (blockStyle, dropRight) =
(str.charAt(len - 1), if (len > 2) Some(str.charAt(len - 2)) else None) match {
(str.charAt(len - 1), if (len > 1) Some(str.charAt(len - 2)) else None) match {
case ('\n', Some('\n')) => (s"|$blockOffsetNumeral+", 1)
case ('\n', _) => (s"|$blockOffsetNumeral", 1)
case (_, _) => (s"|$blockOffsetNumeral-", 0)
Expand Down
4 changes: 2 additions & 2 deletions sjsonnet/src/sjsonnet/ScopedExprTransform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ScopedExprTransform extends ExprTransform {
case Function(pos, params, body) =>
nestedNames(params.names)(rec(e))

case ObjComp(pos, preLocals, key, value, plus, postLocals, first, rest) =>
case ObjComp(pos, preLocals, key, value, plus, visibility, postLocals, first, rest) =>
val (f2 :: r2, (k2, (pre2, post2, v2))) = compSpecs(
first :: rest,
{ () =>
Expand All @@ -55,7 +55,7 @@ class ScopedExprTransform extends ExprTransform {
rest
).zipped.forall(_ eq _): @nowarn
) e
else ObjComp(pos, pre2, k2, v2, plus, post2, f2.asInstanceOf[ForSpec], r2)
else ObjComp(pos, pre2, k2, v2, plus, visibility, post2, f2.asInstanceOf[ForSpec], r2)

case Comp(pos, value, first, rest) =>
val (f2 :: r2, v2) = compSpecs(first :: rest.toList, () => transform(value)): @unchecked
Expand Down
4 changes: 3 additions & 1 deletion sjsonnet/src/sjsonnet/TomlRenderer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ class TomlRenderer(
case d if java.lang.Double.isNaN(d) => out.write("nan")
case d if java.lang.Double.compare(d, -0.0) == 0 => out.write("-0")
case d if math.round(d).toDouble == d => out.write(java.lang.Long.toString(d.toLong))
case d => out.write(java.lang.Double.toString(d))
case d if d % 1 == 0 =>
out.write(BigDecimal(d).setScale(0, BigDecimal.RoundingMode.HALF_EVEN).toBigInt.toString())
case d => out.write(java.lang.Double.toString(d))
}
flush
}
Expand Down
2 changes: 1 addition & 1 deletion sjsonnet/src/sjsonnet/Val.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2287,7 +2287,7 @@ object Val {
case (_, _: Val.Null) =>
Error.fail("Cannot merge " + l.prettyName + " with null", pos)
case _ =>
throw new MatchError((l, r))
Error.fail("Cannot merge " + l.prettyName + " with " + r.prettyName, pos)
}

def valueRaw(k: String, self: Obj, pos: Position, cacheOwner: Obj = null, cacheKey: Any = null)(
Expand Down
Loading
Loading