diff --git a/sjsonnet/src-jvm/sjsonnet/Platform.scala b/sjsonnet/src-jvm/sjsonnet/Platform.scala index 4ba3e07b..e1fdc250 100644 --- a/sjsonnet/src-jvm/sjsonnet/Platform.scala +++ b/sjsonnet/src-jvm/sjsonnet/Platform.scala @@ -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.* @@ -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) } diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index bb7632df..e41deda5 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -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 @@ -240,7 +241,7 @@ 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) } @@ -248,9 +249,9 @@ class Evaluator( 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 @@ -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). @@ -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) @@ -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_/ => @@ -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))) @@ -1916,6 +1937,7 @@ class Evaluator( case Val.Str(_, k) => val member = new ObjCompMember( e.plus, + e.visibility, this, binds, s, @@ -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 { diff --git a/sjsonnet/src/sjsonnet/Expr.scala b/sjsonnet/src/sjsonnet/Expr.scala index 9a7cb53d..b4e4fe85 100644 --- a/sjsonnet/src/sjsonnet/Expr.scala +++ b/sjsonnet/src/sjsonnet/Expr.scala @@ -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]) diff --git a/sjsonnet/src/sjsonnet/ExprTransform.scala b/sjsonnet/src/sjsonnet/ExprTransform.scala index af56fba2..2f2256d6 100644 --- a/sjsonnet/src/sjsonnet/ExprTransform.scala +++ b/sjsonnet/src/sjsonnet/ExprTransform.scala @@ -166,7 +166,7 @@ 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) @@ -174,7 +174,7 @@ abstract class ExprTransform { 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) diff --git a/sjsonnet/src/sjsonnet/Parser.scala b/sjsonnet/src/sjsonnet/Parser.scala index 07987c6e..a1589bd3 100644 --- a/sjsonnet/src/sjsonnet/Parser.scala +++ b/sjsonnet/src/sjsonnet/Parser.scala @@ -887,7 +887,7 @@ class Parser( Expr.FieldName.Dyn(lhs), plus, args, - Visibility.Normal, + visibility, rhsBody ) => val rhs = if (args == null) { @@ -920,6 +920,7 @@ class Parser( lhs, rhs, plus, + visibility, postLocals.toArray, comps._1, comps._2.toList diff --git a/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala b/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala index 8d387f88..743a8db5 100644 --- a/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala +++ b/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala @@ -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) diff --git a/sjsonnet/src/sjsonnet/ScopedExprTransform.scala b/sjsonnet/src/sjsonnet/ScopedExprTransform.scala index 7a723400..b26fd79f 100644 --- a/sjsonnet/src/sjsonnet/ScopedExprTransform.scala +++ b/sjsonnet/src/sjsonnet/ScopedExprTransform.scala @@ -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, { () => @@ -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 diff --git a/sjsonnet/src/sjsonnet/TomlRenderer.scala b/sjsonnet/src/sjsonnet/TomlRenderer.scala index 92d88a5e..a13256ce 100644 --- a/sjsonnet/src/sjsonnet/TomlRenderer.scala +++ b/sjsonnet/src/sjsonnet/TomlRenderer.scala @@ -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 } diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index fdd0b9dc..65fbc557 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -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)( diff --git a/sjsonnet/src/sjsonnet/YamlRenderer.scala b/sjsonnet/src/sjsonnet/YamlRenderer.scala index 07dff93b..43255998 100644 --- a/sjsonnet/src/sjsonnet/YamlRenderer.scala +++ b/sjsonnet/src/sjsonnet/YamlRenderer.scala @@ -46,13 +46,18 @@ class YamlRenderer( elemBuilder.append('"') elemBuilder.append('"') } else if (s.charAt(len - 1) == '\n') { - val splits = YamlRenderer.newlinePattern.split(s.toString) - elemBuilder.append('|') + val str = s.toString + val splits = YamlRenderer.newlinePattern.split(str, -1) + val blockOffsetNumeral = if (str.charAt(0) != ' ') "" else indent + val (blockStyle, dropRight) = + if (len > 1 && str.charAt(len - 2) == '\n') (s"|${blockOffsetNumeral}+", 1) + else (s"|${blockOffsetNumeral}", 1) + appendString(blockStyle) depth += 1 - splits.foreach { split => + splits.dropRight(dropRight).foreach { split => newlineBuffered = true flushBuffer() - appendString(split) // TODO escaping? + appendString(split) } depth -= 1 } else { diff --git a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala index a472a28e..a33042cf 100644 --- a/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/ArrayModule.scala @@ -1076,15 +1076,19 @@ object ArrayModule extends AbstractFunctionModule { * * Remove element at idx index from arr. */ - builtin("removeAt", "arr", "idx") { (_, _, arr: Val.Arr, idx: Val) => + builtin("removeAt", "arr", "idx") { (pos, ev, arr: Val.Arr, idx: Val) => val removeIdx = idx match { case n: Val.Num => val d = n.asDouble - if (d.isWhole && d >= 0 && d < arr.length) d.toInt else -1 - case _ => -1 + if (!d.isWhole) + Error.fail("idx must be an integer, got " + d, pos)(ev) + if (d < 0 || d >= arr.length) + Error.fail("idx out of bounds", pos)(ev) + d.toInt + case _ => + Error.fail("idx must be a number, got " + idx.value.prettyName, pos)(ev) } - if (removeIdx == -1) arr - else removeAtView(arr, removeIdx) + removeAtView(arr, removeIdx) }, /** * [[https://jsonnet.org/ref/stdlib.html#std-sum std.sum(arr)]]. diff --git a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala index 16d6f5e4..a021074d 100644 --- a/sjsonnet/src/sjsonnet/stdlib/MathModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/MathModule.scala @@ -292,7 +292,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.pow(x, n) as a mathematical function. */ builtin("pow", "x", "n") { (pos, ev, x: Double, n: Double) => - math.pow(x, n) + val r = math.pow(x, n) + if (java.lang.Double.isNaN(r)) Error.fail("not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.floor(x)]]. @@ -424,7 +426,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.asin(x) as a mathematical function. */ builtin("asin", "x") { (pos, ev, x: Double) => - math.asin(x) + val r = math.asin(x) + if (java.lang.Double.isNaN(r)) Error.fail("not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.acos(x)]]. @@ -434,7 +438,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.acos(x) as a mathematical function. */ builtin("acos", "x") { (pos, ev, x: Double) => - math.acos(x) + val r = math.acos(x) + if (java.lang.Double.isNaN(r)) Error.fail("not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.atan(x)]]. @@ -494,7 +500,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.log(x) as a mathematical function. */ builtin("log", "x") { (pos, ev, x: Double) => - math.log(x) + val r = math.log(x) + if (java.lang.Double.isNaN(r)) Error.fail("Not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.log2(x)]]. @@ -504,8 +512,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.log2(x) as a mathematical function. */ builtin("log2", "x") { (pos, ev, x: Double) => - // no scala log2, do our best without getting fancy with numerics - math.log(x) / math.log(2.0) + val r = math.log(x) / math.log(2.0) + if (java.lang.Double.isNaN(r)) Error.fail("Not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.log10(x)]]. @@ -515,7 +524,9 @@ object MathModule extends AbstractFunctionModule { * The official docs list std.log10(x) as a mathematical function. */ builtin("log10", "x") { (pos, ev, x: Double) => - math.log10(x) + val r = math.log10(x) + if (java.lang.Double.isNaN(r)) Error.fail("Not a number", pos)(ev) + r }, /** * [[https://jsonnet.org/ref/stdlib.html#math std.exp(x)]]. diff --git a/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala b/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala index df06fe2c..e1777a01 100644 --- a/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala +++ b/sjsonnet/src/sjsonnet/stdlib/NativeRegex.scala @@ -26,7 +26,11 @@ object NativeRegex extends AbstractFunctionModule { Val.Obj.mk( pos.noOffset, - "string" -> new Obj.ConstMember(true, Visibility.Normal, Val.Str(pos.noOffset, str)), + "string" -> new Obj.ConstMember( + true, + Visibility.Normal, + Val.Str(pos.noOffset, str.substring(matcher.start(), matcher.end())) + ), "captures" -> new Obj.ConstMember( true, Visibility.Normal, diff --git a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala index 6c9e95a0..8215ebaf 100644 --- a/sjsonnet/src/sjsonnet/stdlib/StringModule.scala +++ b/sjsonnet/src/sjsonnet/stdlib/StringModule.scala @@ -1314,14 +1314,16 @@ object StringModule extends AbstractFunctionModule { * * Returns true if the given string is of zero length. */ - builtin("isEmpty", "str") { (_, _, value: Val) => + builtin("isEmpty", "str") { (pos, ev, value: Val) => value match { case Val.Str(_, s) => s.isEmpty case a: Val.Arr => a.length == 0 case o: Val.Obj => o.visibleKeyNames.isEmpty - case f: Val.Func => f.params.names.isEmpty case x => - Error.fail("length operates on strings, objects, and arrays, got " + x.prettyName) + Error.fail( + "isEmpty operates on strings, objects, and arrays, got " + x.prettyName, + pos + )(ev) } }, /** diff --git a/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden index f38106a0..bf7c00b0 100644 --- a/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtinIsEmpty2.jsonnet.golden @@ -1,2 +1,2 @@ -sjsonnet.Error: [std.isEmpty] length operates on strings, objects, and arrays, got number +sjsonnet.Error: [std.isEmpty] isEmpty operates on strings, objects, and arrays, got number at [].(builtinIsEmpty2.jsonnet:1:12) diff --git a/sjsonnet/test/resources/go_test_suite/builtin_log7.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtin_log7.jsonnet.golden index fae96456..c2316816 100644 --- a/sjsonnet/test/resources/go_test_suite/builtin_log7.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtin_log7.jsonnet.golden @@ -1,3 +1,3 @@ -sjsonnet.Error: not a number +sjsonnet.Error: [std.log] Not a number at [].(builtin_log7.jsonnet:1:8) diff --git a/sjsonnet/test/resources/go_test_suite/builtin_log8.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/builtin_log8.jsonnet.golden index 53dce451..b2321a2d 100644 --- a/sjsonnet/test/resources/go_test_suite/builtin_log8.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/builtin_log8.jsonnet.golden @@ -1,3 +1,3 @@ -sjsonnet.Error: not a number +sjsonnet.Error: [std.log] Not a number at [].(builtin_log8.jsonnet:1:8) diff --git a/sjsonnet/test/resources/go_test_suite/parseYaml.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/parseYaml.jsonnet.golden index 6082f337..a5eb13dd 100644 --- a/sjsonnet/test/resources/go_test_suite/parseYaml.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/parseYaml.jsonnet.golden @@ -52,5 +52,7 @@ null, 2 ], - null + [ + null + ] ] diff --git a/sjsonnet/test/resources/go_test_suite/pow4.jsonnet.golden b/sjsonnet/test/resources/go_test_suite/pow4.jsonnet.golden index 447a0902..b54116a4 100644 --- a/sjsonnet/test/resources/go_test_suite/pow4.jsonnet.golden +++ b/sjsonnet/test/resources/go_test_suite/pow4.jsonnet.golden @@ -1,3 +1,2 @@ -sjsonnet.Error: not a number +sjsonnet.Error: [std.pow] not a number at [].(pow4.jsonnet:1:8) - diff --git a/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet b/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet new file mode 100644 index 00000000..bf389610 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet @@ -0,0 +1,5 @@ +// Test that normal arithmetic operations still work correctly. +assert 1 + 2 == 3; +assert 3 * 4 == 12; +assert 10 - 3 == 7; +true diff --git a/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/arithmetic_normal_operations.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet new file mode 100644 index 00000000..87214820 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet @@ -0,0 +1,2 @@ +// Test that addition overflow (Infinity) errors instead of silently propagating. +1e308 + 1e308 diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet.golden new file mode 100644 index 00000000..d6cfd626 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_addition.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: overflow + at [].(error.arithmetic_overflow_addition.jsonnet:2:7) diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet new file mode 100644 index 00000000..0ee10de9 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet @@ -0,0 +1,2 @@ +// Test that multiplication overflow (Infinity) errors instead of silently propagating. +1e308 * 1e308 diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet.golden new file mode 100644 index 00000000..47400074 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_multiplication.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: overflow + at [].(error.arithmetic_overflow_multiplication.jsonnet:2:7) diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet new file mode 100644 index 00000000..55906eda --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet @@ -0,0 +1,2 @@ +// Test that subtraction overflow (Infinity) errors instead of silently propagating. +-1e308 - 1e308 diff --git a/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet.golden new file mode 100644 index 00000000..43eeae58 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.arithmetic_overflow_subtraction.jsonnet.golden @@ -0,0 +1,2 @@ +sjsonnet.Error: overflow + at [].(error.arithmetic_overflow_subtraction.jsonnet:2:8) diff --git a/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet b/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet new file mode 100644 index 00000000..c9fe4f38 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet @@ -0,0 +1,2 @@ +// std.isEmpty should error on function type +std.isEmpty(function() true) diff --git a/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet.golden new file mode 100644 index 00000000..4393a1a8 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.isEmpty_function_type.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.isEmpty] isEmpty operates on strings, objects, and arrays, got function diff --git a/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet b/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet new file mode 100644 index 00000000..1c5fe61e --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet @@ -0,0 +1,2 @@ +// std.isEmpty should error on number type +std.isEmpty(42) diff --git a/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet.golden new file mode 100644 index 00000000..53b0225b --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.isEmpty_number_type.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.isEmpty] isEmpty operates on strings, objects, and arrays, got number diff --git a/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet b/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet new file mode 100644 index 00000000..07258bd6 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet @@ -0,0 +1,2 @@ +// std.acos out of domain should error with "not a number" +std.acos(2) diff --git a/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet.golden new file mode 100644 index 00000000..08f406e0 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_acos_nan.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.acos] not a number diff --git a/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet b/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet new file mode 100644 index 00000000..0f9a8abd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet @@ -0,0 +1,2 @@ +// std.asin out of domain should error with "not a number" +std.asin(2) diff --git a/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet.golden new file mode 100644 index 00000000..91f57ec7 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_asin_nan.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.asin] not a number diff --git a/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet b/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet new file mode 100644 index 00000000..d758e76b --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet @@ -0,0 +1,2 @@ +// std.pow with negative base and fractional exponent should error with "not a number" +std.pow(-1, 0.5) diff --git a/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet.golden new file mode 100644 index 00000000..6de87c33 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.math_pow_nan.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.pow] not a number diff --git a/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet new file mode 100644 index 00000000..e8cc29b4 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet @@ -0,0 +1,2 @@ +// Incompatible types (boolean + number) should produce descriptive error, not MatchError +({a: true} + {a+: 1}).a diff --git a/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet.golden new file mode 100644 index 00000000..0dd4824d --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_boolean_number.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: Cannot merge boolean with number diff --git a/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet new file mode 100644 index 00000000..8be7c3f0 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet @@ -0,0 +1,2 @@ +// Incompatible types (number + boolean) should produce descriptive error, not MatchError +({a: 1} + {a+: true}).a diff --git a/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet.golden new file mode 100644 index 00000000..bb3134ee --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.plus_colon_merge_number_boolean.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: Cannot merge number with boolean diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet b/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet new file mode 100644 index 00000000..93bcc85e --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet @@ -0,0 +1 @@ +std.removeAt([1, 2, 3], -1) diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet.golden new file mode 100644 index 00000000..80343aea --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_negative_index.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.removeAt] idx out of bounds diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet new file mode 100644 index 00000000..287dbaa8 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet @@ -0,0 +1 @@ +std.removeAt([1, 2, 3], 1.5) diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet.golden new file mode 100644 index 00000000..53e6e28e --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_integer.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.removeAt] idx must be an integer, got 1.5 diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet new file mode 100644 index 00000000..50ceeb51 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet @@ -0,0 +1 @@ +std.removeAt([1, 2, 3], "a") diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet.golden new file mode 100644 index 00000000..cb96b15f --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_non_number.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.removeAt] idx must be a number, got string diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet b/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet new file mode 100644 index 00000000..19b4715e --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet @@ -0,0 +1 @@ +std.removeAt([1, 2, 3], 10) diff --git a/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet.golden new file mode 100644 index 00000000..80343aea --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/error.removeAt_out_of_bounds.jsonnet.golden @@ -0,0 +1 @@ +sjsonnet.Error: [std.removeAt] idx out of bounds diff --git a/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet b/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet new file mode 100644 index 00000000..65fd5353 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet @@ -0,0 +1,6 @@ +// std.isEmpty should work for valid types and error on invalid types +assert std.isEmpty("") == true : "empty string"; +assert std.isEmpty("hello") == false : "non-empty string"; +assert std.isEmpty([]) == true : "empty array"; +assert std.isEmpty({}) == true : "empty object"; +true diff --git a/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/isEmpty_valid_types.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet b/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet new file mode 100644 index 00000000..1eda145d --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet @@ -0,0 +1,5 @@ +// std.asin, std.acos, and std.pow with valid inputs still work +assert std.asin(0) == 0 : "std.asin(0) should be 0"; +assert std.acos(1) == 0 : "std.acos(1) should be 0"; +assert std.pow(2, 3) == 8 : "std.pow(2, 3) should be 8"; +true diff --git a/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/math_nan_checks_success.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet b/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet new file mode 100644 index 00000000..92eb78d5 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet @@ -0,0 +1,9 @@ +// Object comprehension visibility modifiers: :: (hidden), ::: (forced), : (normal) +// :: (hidden) - invisible to objectFields, visible to objectFieldsAll +assert std.objectFields({[k]:: 1 for k in ["a", "b"]}) == [] : "hidden fields invisible to objectFields"; +assert std.objectFieldsAll({[k]:: 1 for k in ["a", "b"]}) == ["a", "b"] : "hidden fields visible to objectFieldsAll"; +// ::: (forced) - always visible +assert std.objectFields({[k]::: 1 for k in ["a", "b"]}) == ["a", "b"] : "forced visibility"; +// : (normal) - standard visibility +assert std.objectFields({[k]: 1 for k in ["a", "b"]}) == ["a", "b"] : "normal visibility"; +true diff --git a/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/object_comprehension_visibility.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet b/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet new file mode 100644 index 00000000..506bac3a --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet @@ -0,0 +1,9 @@ +// Test that explicit --- document start markers cause single-doc YAML +// to be wrapped in an array, matching go-jsonnet behavior. +std.assertEqual(std.parseYaml("---"), [null]) && +std.assertEqual(std.parseYaml("---\n"), [null]) && +std.assertEqual(std.parseYaml("---\na: 1"), [{a: 1}]) && +std.assertEqual(std.parseYaml("--- 3\n"), [3]) && +std.assertEqual(std.parseYaml("---a: 1"), {"---a": 1}) && +std.assertEqual(std.parseYaml("a: 1"), {a: 1}) && +true diff --git a/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/parseyaml_doc_marker.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet b/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet new file mode 100644 index 00000000..e40a6d0f --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet @@ -0,0 +1,20 @@ +// Test YAML 1.2 modern octal syntax (0o prefix) for unquoted scalars. +// Quoted values must remain strings. Legacy octal (0 prefix) still works. +local yaml = std.parseYaml(||| + a: 0777 + b: 0o777 + c: 0 + d: 0o10 + e: -0o777 + f: "0o777" + g: '0o777' +|||); + +std.assertEqual(yaml.a, 511) && +std.assertEqual(yaml.b, 511) && +std.assertEqual(yaml.c, 0) && +std.assertEqual(yaml.d, 8) && +std.assertEqual(yaml.e, -511) && +std.assertEqual(yaml.f, "0o777") && +std.assertEqual(yaml.g, "0o777") && +true diff --git a/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/parseyaml_yaml12_octal.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet b/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet new file mode 100644 index 00000000..642a68d2 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet @@ -0,0 +1,5 @@ +// Test +: merge with compatible types +assert ({a: "hello"} + {a+: " world"}).a == "hello world"; +assert ({a: 1} + {a+: 2}).a == 3; +assert ({a: [1]} + {a+: [2]}).a == [1, 2]; +true diff --git a/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/plus_colon_merge_type_success.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/regex-js.jsonnet b/sjsonnet/test/resources/new_test_suite/regex-js.jsonnet index 16150a25..dc7b90f3 100644 --- a/sjsonnet/test/resources/new_test_suite/regex-js.jsonnet +++ b/sjsonnet/test/resources/new_test_suite/regex-js.jsonnet @@ -34,7 +34,7 @@ std.assertEqual(std.native('regexPartialMatch')(@'world', 'hello'), null) && std.assertEqual( std.native('regexPartialMatch')(@'e', 'hello'), { - string: 'hello', + string: 'e', captures: [], namedCaptures: {}, } @@ -43,7 +43,7 @@ std.assertEqual( std.assertEqual( std.native('regexPartialMatch')(@'e(.*)o', 'hello'), { - string: 'hello', + string: 'ello', captures: ['ll'], namedCaptures: {}, } @@ -52,7 +52,7 @@ std.assertEqual( std.assertEqual( std.native('regexPartialMatch')(@'e(?P.*)o', 'hello'), { - string: 'hello', + string: 'ello', captures: ['ll'], namedCaptures: { mid: 'll', diff --git a/sjsonnet/test/resources/new_test_suite/regex-jvm-native.jsonnet b/sjsonnet/test/resources/new_test_suite/regex-jvm-native.jsonnet index a5d599d1..adafd243 100644 --- a/sjsonnet/test/resources/new_test_suite/regex-jvm-native.jsonnet +++ b/sjsonnet/test/resources/new_test_suite/regex-jvm-native.jsonnet @@ -34,7 +34,7 @@ std.assertEqual(std.native('regexPartialMatch')(@'world', 'hello'), null) && std.assertEqual( std.native('regexPartialMatch')(@'e', 'hello'), { - string: 'hello', + string: 'e', captures: [], namedCaptures: {}, } @@ -43,7 +43,7 @@ std.assertEqual( std.assertEqual( std.native('regexPartialMatch')(@'e(.*)o', 'hello'), { - string: 'hello', + string: 'ello', captures: ['ll'], namedCaptures: {}, } @@ -52,7 +52,7 @@ std.assertEqual( std.assertEqual( std.native('regexPartialMatch')(@'e(?P.*)o', 'hello'), { - string: 'hello', + string: 'ello', captures: ['ll'], namedCaptures: { mid: 'll', diff --git a/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet b/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet new file mode 100644 index 00000000..bc6dc95c --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet @@ -0,0 +1,8 @@ +// std.regexPartialMatch string field should return matched substring, not full input +local r1 = std.regexPartialMatch("foo", "foobar"); +local r2 = std.regexPartialMatch("[0-9]+", "abc123def"); +local r3 = std.regexFullMatch("foo", "foo"); +assert r1.string == "foo" : "partial match substring"; +assert r2.string == "123" : "digit match substring"; +assert r3.string == "foo" : "full match substring"; +true diff --git a/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/regex_partial_match_string_field.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet b/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet new file mode 100644 index 00000000..687727c2 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet @@ -0,0 +1,5 @@ +// std.removeAt with valid index works correctly +assert std.removeAt([1, 2, 3], 1) == [1, 3] : "remove middle element"; +assert std.removeAt([1, 2, 3], 0) == [2, 3] : "remove first element"; +assert std.removeAt([1, 2, 3], 2) == [1, 2] : "remove last element"; +true diff --git a/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/removeAt_valid.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet b/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet new file mode 100644 index 00000000..3735f470 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet @@ -0,0 +1,9 @@ +// TomlRenderer renders large integers as decimal instead of scientific notation +local large = std.manifestToml({a: 1e20}); +assert std.length(std.findSubstr("E", large)) == 0 : "Large integer must not contain scientific notation 'E'"; +assert std.length(std.findSubstr("100000000000000000000", large)) > 0 : "Large integer must render as decimal"; +// Regular integers still work +assert std.manifestToml({a: 42}) == "a = 42" : "Regular integer must render correctly"; +// Fractions still render as floats +assert std.manifestToml({a: 3.14}) == "a = 3.14" : "Float must render correctly"; +true diff --git a/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/toml_renderer_large_integer.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet b/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet new file mode 100644 index 00000000..1336ac49 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet @@ -0,0 +1,6 @@ +// Test that tryEagerEval preserves lazy semantics: unused local bindings with side effects +// should not be forced. This matches go-jsonnet and jrsonnet behavior. +std.assertEqual( + (local a = error "should not be evaluated"; local b = a + 1; if false then b else 0), + 0 +) diff --git a/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/tryEagerEval_lazy_thunks.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet new file mode 100644 index 00000000..0999269a --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet @@ -0,0 +1,9 @@ +// Test YAML block scalar leading whitespace handling. +// Strings with leading whitespace require an indent indicator (e.g., |2). +// Strings without leading whitespace should not have an indent indicator. +local contains(haystack, needle) = std.length(std.findSubstr(needle, haystack)) > 0; +local leading = std.manifestYamlDoc(" foo\n bar\n"); +local noLeading = std.manifestYamlDoc("foo\nbar\n"); +assert contains(leading, "|2") : "leading whitespace requires indent indicator |2"; +assert !contains(noLeading, "|2") : "no leading whitespace means no indent indicator"; +true diff --git a/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_leading_whitespace.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet new file mode 100644 index 00000000..5ce18774 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet @@ -0,0 +1,10 @@ +// Test YAML block scalar trailing newline handling. +// Single trailing newline should use clip mode (|), not keep mode (|+). +// Multiple trailing newlines should use keep mode (|+). +local contains(haystack, needle) = std.length(std.findSubstr(needle, haystack)) > 0; +local singleNl = std.manifestYamlDoc("foo\nbar\n"); +local multiNl = std.manifestYamlDoc("foo\nbar\n\n"); +assert contains(singleNl, "|") : "single trailing newline should use clip mode |"; +assert !contains(singleNl, "|+") : "single trailing newline should not use keep mode |+"; +assert contains(multiNl, "|+") : "multiple trailing newlines should use keep mode |+"; +true diff --git a/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet.golden new file mode 100644 index 00000000..27ba77dd --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/yaml_renderer_block_scalar_trailing_newlines.jsonnet.golden @@ -0,0 +1 @@ +true diff --git a/sjsonnet/test/src/sjsonnet/ParseYamlTests.scala b/sjsonnet/test/src/sjsonnet/ParseYamlTests.scala index 880bc285..bf9be541 100644 --- a/sjsonnet/test/src/sjsonnet/ParseYamlTests.scala +++ b/sjsonnet/test/src/sjsonnet/ParseYamlTests.scala @@ -46,12 +46,12 @@ object ParseYamlTests extends TestSuite { } test { // Scalar documents can start on the same line as the document-start marker - // "--- 3" as standalone - eval("std.parseYaml('--- 3\\n')") ==> ujson.Value("""3""") + // "--- 3" as standalone (explicit doc start → always array) + eval("std.parseYaml('--- 3\\n')") ==> ujson.Value("""[3]""") } test { - // Folded scalar as document - eval("std.parseYaml('--- >\\n hello\\n world\\n')") ==> ujson.Value(""""hello world\n"""") + // Folded scalar as document (explicit doc start → always array) + eval("std.parseYaml('--- >\\n hello\\n world\\n')") ==> ujson.Value("""["hello world\n"]""") } test { // Combined: scalar docs on same line as marker @@ -66,8 +66,8 @@ object ParseYamlTests extends TestSuite { ) } test { - // Bare document separator - eval("""std.parseYaml("---")""") ==> ujson.Value("""null""") + // Bare document separator → explicit doc start, always returns array + eval("""std.parseYaml("---")""") ==> ujson.Value("""[null]""") } test { // Folded scalar without document marker (directly) diff --git a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala index 5af4b51b..d808c372 100644 --- a/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala +++ b/sjsonnet/test/src/sjsonnet/Std0150FunctionsTests.scala @@ -241,7 +241,7 @@ object Std0150FunctionsTests extends TestSuite { assert( evalErr("""std.isEmpty(10)""") .startsWith( - "sjsonnet.Error: [std.isEmpty] length operates on strings, objects, and arrays, got number" + "sjsonnet.Error: [std.isEmpty] isEmpty operates on strings, objects, and arrays, got number" ) ) } diff --git a/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala b/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala index ea4ec97f..cf065211 100644 --- a/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala +++ b/sjsonnet/test/src/sjsonnet/StdLibOfficialCompatibilityTests.scala @@ -27,19 +27,18 @@ object StdLibOfficialCompatibilityTests extends TestSuite { test("removeAt filters by exact index equality") { eval("""std.removeAt([1, 2, 3], 1)""") ==> ujson.Arr(1, 3) - eval("""std.removeAt([1, 2, 3], 1.5)""") ==> ujson.Arr(1, 2, 3) - eval("""std.removeAt([1, 2, 3], -1)""") ==> ujson.Arr(1, 2, 3) - eval("""std.removeAt([1, 2, 3], 9)""") ==> ujson.Arr(1, 2, 3) - eval("""std.removeAt([1, 2, 3], "1")""") ==> ujson.Arr(1, 2, 3) + assert(evalErr("""std.removeAt([1, 2, 3], 1.5)""").contains("idx must be an integer")) + assert(evalErr("""std.removeAt([1, 2, 3], -1)""").contains("idx out of bounds")) + assert(evalErr("""std.removeAt([1, 2, 3], 9)""").contains("idx out of bounds")) + assert(evalErr("""std.removeAt([1, 2, 3], "1")""").contains("idx must be a number")) } test("isEmpty delegates to std.length") { eval("""std.isEmpty("")""") ==> ujson.True eval("""std.isEmpty([])""") ==> ujson.True eval("""std.isEmpty({})""") ==> ujson.True - eval("""std.isEmpty(function() 1)""") ==> ujson.True - eval("""std.isEmpty(function(a, b) a)""") ==> ujson.False - assert(evalErr("""std.isEmpty(10)""").contains("length operates on strings")) + assert(evalErr("""std.isEmpty(function() 1)""").contains("isEmpty operates on strings")) + assert(evalErr("""std.isEmpty(10)""").contains("isEmpty operates on strings")) } test("escape string helpers stringify non-string inputs") { diff --git a/sjsonnet/test/src/sjsonnet/StdMathTests.scala b/sjsonnet/test/src/sjsonnet/StdMathTests.scala index be9ae1a7..816a7d76 100644 --- a/sjsonnet/test/src/sjsonnet/StdMathTests.scala +++ b/sjsonnet/test/src/sjsonnet/StdMathTests.scala @@ -48,5 +48,23 @@ object StdMathTests extends TestSuite { eval("std.sqrt(0)") ==> ujson.Num(0.0) eval("std.sqrt(4)") ==> ujson.Num(2.0) } + test("log and log2 reject negative and zero input") { + // go-jsonnet: makeDoubleCheck returns "Not a number" for NaN, Val.Num catches Infinity as "overflow" + val errLog = evalErr("std.log(-1)") + assert(errLog.contains("Not a number")) + val errLog2 = evalErr("std.log2(-1)") + assert(errLog2.contains("Not a number")) + val errLog10 = evalErr("std.log10(-1)") + assert(errLog10.contains("Not a number")) + val errLog0 = evalErr("std.log(0)") + assert(errLog0.contains("overflow")) + val errLog2Zero = evalErr("std.log2(0)") + assert(errLog2Zero.contains("overflow")) + // log(positive) must still work + eval("std.log(1)") ==> ujson.Num(0.0) + eval("std.log2(1)") ==> ujson.Num(0.0) + eval("std.log2(8)") ==> ujson.Num(3.0) + eval("std.log10(100)") ==> ujson.Num(2.0) + } } }