From a9005b32776bae0b9c9380cf6a5b1a5c2dffdcc5 Mon Sep 17 00:00:00 2001
From: Laureline David <laureline@swissborg.com>
Date: Mon, 21 Oct 2024 09:44:26 +0200
Subject: [PATCH] Scala 3 Migration (#1)

Migrates the code to Scala 3 and enables cross-compilation with Scala 2.13.
---
 .../sjsonnet/MaterializerBenchmark.scala      |   5 +-
 .../scala/sjsonnet/OptimizerBenchmark.scala   |  10 +-
 .../main/scala/sjsonnet/ParserBenchmark.scala |   9 +-
 build.sbt                                     |  60 ++++++--
 project/plugins.sbt                           |   3 +-
 .../sjsonnet/CachedResolvedFile.scala         |   4 +-
 sjsonnet/src-jvm-native/sjsonnet/OsPath.scala |   2 +-
 .../sjsonnet/SjsonnetMain.scala               |  14 +-
 sjsonnet/src-jvm/sjsonnet/Platform.scala      |  17 ++-
 sjsonnet/src/sjsonnet/BaseCharRenderer.scala  |  12 +-
 sjsonnet/src/sjsonnet/BaseRenderer.scala      |   2 +-
 sjsonnet/src/sjsonnet/DecimalFormat.scala     |   8 +-
 sjsonnet/src/sjsonnet/Error.scala             |   8 +-
 sjsonnet/src/sjsonnet/Evaluator.scala         |  27 ++--
 sjsonnet/src/sjsonnet/Expr.scala              |   4 +-
 sjsonnet/src/sjsonnet/ExprTransform.scala     |  26 ++--
 sjsonnet/src/sjsonnet/Format.scala            |  25 ++--
 sjsonnet/src/sjsonnet/Importer.scala          |  13 +-
 sjsonnet/src/sjsonnet/Interpreter.scala       |   2 +-
 sjsonnet/src/sjsonnet/Materializer.scala      |   2 +-
 sjsonnet/src/sjsonnet/Parser.scala            | 132 +++++++++---------
 .../src/sjsonnet/PrettyYamlRenderer.scala     |  36 ++---
 sjsonnet/src/sjsonnet/Renderer.scala          |   6 +-
 .../src/sjsonnet/ScopedExprTransform.scala    |  28 ++--
 sjsonnet/src/sjsonnet/StaticOptimizer.scala   |   6 +-
 sjsonnet/src/sjsonnet/Std.scala               |  18 +--
 sjsonnet/src/sjsonnet/Val.scala               |  24 ++--
 sjsonnet/src/sjsonnet/ValScope.scala          |   5 +-
 sjsonnet/src/sjsonnet/ValVisitor.scala        |   8 +-
 sjsonnet/src/sjsonnet/YamlRenderer.scala      |   6 +-
 .../BufferedRandomAccessFileTests.scala       |   4 +-
 sjsonnet/test/src/sjsonnet/DummyPath.scala    |   4 +-
 sjsonnet/test/src/sjsonnet/OldRenderer.scala  |   2 +-
 .../test/src/sjsonnet/OldYamlRenderer.scala   |   2 +-
 sjsonnet/test/src/sjsonnet/ParserTests.scala  |   4 +-
 35 files changed, 293 insertions(+), 245 deletions(-)

diff --git a/bench/src/main/scala/sjsonnet/MaterializerBenchmark.scala b/bench/src/main/scala/sjsonnet/MaterializerBenchmark.scala
index 9152f6f0..edb75417 100644
--- a/bench/src/main/scala/sjsonnet/MaterializerBenchmark.scala
+++ b/bench/src/main/scala/sjsonnet/MaterializerBenchmark.scala
@@ -6,6 +6,7 @@ import java.util.concurrent.TimeUnit
 import org.openjdk.jmh.annotations._
 import org.openjdk.jmh.infra._
 import ujson.JsVisitor
+import scala.compiletime.uninitialized
 
 @BenchmarkMode(Array(Mode.AverageTime))
 @Fork(2)
@@ -16,8 +17,8 @@ import ujson.JsVisitor
 @State(Scope.Benchmark)
 class MaterializerBenchmark {
 
-  private var interp: Interpreter = _
-  private var value: Val = _
+  private var interp: Interpreter = uninitialized
+  private var value: Val = uninitialized
 
   @Setup
   def setup(): Unit = {
diff --git a/bench/src/main/scala/sjsonnet/OptimizerBenchmark.scala b/bench/src/main/scala/sjsonnet/OptimizerBenchmark.scala
index 2e95de54..8b55a952 100644
--- a/bench/src/main/scala/sjsonnet/OptimizerBenchmark.scala
+++ b/bench/src/main/scala/sjsonnet/OptimizerBenchmark.scala
@@ -10,6 +10,7 @@ import org.openjdk.jmh.annotations._
 import org.openjdk.jmh.infra._
 
 import scala.collection.mutable
+import scala.compiletime.uninitialized
 
 @BenchmarkMode(Array(Mode.AverageTime))
 @Fork(2)
@@ -20,16 +21,17 @@ import scala.collection.mutable
 @State(Scope.Benchmark)
 class OptimizerBenchmark {
 
-  private var inputs: Iterable[(Expr, FileScope)] = _
-  private var allFiles: IndexedSeq[(Path, String)] = _
-  private var ev: EvalScope = _
+  private var inputs: Iterable[(Expr, FileScope)] = uninitialized
+  private var allFiles: IndexedSeq[(Path, String)] = uninitialized
+  private var ev: EvalScope = uninitialized
 
   @Setup
   def setup(): Unit = {
     val (allFiles, ev) = MainBenchmark.findFiles()
     this.inputs = allFiles.map { case (p, s) =>
-      fastparse.parse(s, new Parser(p, true, mutable.HashMap.empty, mutable.HashMap.empty).document(_)) match {
+      fastparse.parse(s, new Parser(p, true, mutable.HashMap.empty, mutable.HashMap.empty).document(using _)) match {
         case Success(v, _) => v
+        case _ => throw new RuntimeException("Parse Failed")
       }
     }
     this.ev = ev
diff --git a/bench/src/main/scala/sjsonnet/ParserBenchmark.scala b/bench/src/main/scala/sjsonnet/ParserBenchmark.scala
index 6100534d..b0a2f45e 100644
--- a/bench/src/main/scala/sjsonnet/ParserBenchmark.scala
+++ b/bench/src/main/scala/sjsonnet/ParserBenchmark.scala
@@ -7,6 +7,7 @@ import scala.collection.mutable.HashMap
 import fastparse.Parsed.Success
 import org.openjdk.jmh.annotations._
 import org.openjdk.jmh.infra._
+import scala.compiletime.uninitialized
 
 @BenchmarkMode(Array(Mode.AverageTime))
 @Fork(2)
@@ -17,8 +18,8 @@ import org.openjdk.jmh.infra._
 @State(Scope.Benchmark)
 class ParserBenchmark {
 
-  private var allFiles: IndexedSeq[(Path, String)] = _
-  private var interp: Interpreter = _
+  private var allFiles: IndexedSeq[(Path, String)] = uninitialized
+  private var interp: Interpreter = uninitialized
 
   @Setup
   def setup(): Unit =
@@ -27,8 +28,8 @@ class ParserBenchmark {
   @Benchmark
   def main(bh: Blackhole): Unit = {
     bh.consume(allFiles.foreach { case (p, s) =>
-      val res = fastparse.parse(s, new Parser(p, true, HashMap.empty, HashMap.empty).document(_))
-      bh.consume(res.asInstanceOf[Success[_]])
+      val res = fastparse.parse(s, new Parser(p, true, HashMap.empty, HashMap.empty).document(using _))
+      bh.consume(res.asInstanceOf[Success[?]])
     })
   }
 }
diff --git a/build.sbt b/build.sbt
index b887b9c2..1f45883a 100644
--- a/build.sbt
+++ b/build.sbt
@@ -1,29 +1,55 @@
 val sjsonnetVersion = "0.4.4"
 
-scalaVersion in Global := "2.13.4"
+val scala213 = "2.13.15"
+val scala3 = "3.5.1"
+
+val commonOptions: Seq[String] = Seq(
+  "-opt:l:inline",
+  "-opt-inline-from:sjsonnet.*,sjsonnet.**",
+)
 
 cancelable in Global := true
+publish / skip := true
 
 lazy val main = (project in file("sjsonnet"))
   .settings(
-    Compile / scalacOptions ++= Seq("-opt:l:inline", "-opt-inline-from:sjsonnet.*,sjsonnet.**"),
+    name := "sjsonnet",
+
+    // Enable cross-compilation
+    scalaVersion := scala3,
+    crossScalaVersions := Seq(scala213, scala3),
+    scalacOptions ++= {
+      (CrossVersion.partialVersion(scalaVersion.value) match {
+        case Some((3, _)) =>
+          commonOptions ++ Seq(
+            // options dedicated for cross build / migration to Scala 3
+            "-source:3.5-migration"
+          )
+        case _ =>
+          commonOptions ++ Seq(
+            "-Xsource:3"
+          )
+      })
+    },
+
+
     Test / fork := true,
     Test / baseDirectory := (ThisBuild / baseDirectory).value,
     libraryDependencies ++= Seq(
-      "com.lihaoyi" %% "fastparse" % "2.3.1",
-      "com.lihaoyi" %% "pprint" % "0.6.1",
-      "com.lihaoyi" %% "ujson" % "1.3.7",
-      "com.lihaoyi" %% "scalatags" % "0.9.3",
-      "com.lihaoyi" %% "os-lib" % "0.7.2",
-      "com.lihaoyi" %% "mainargs" % "0.2.0",
+      "com.lihaoyi" %% "fastparse" % "3.1.1",
+      "com.lihaoyi" %% "pprint" % "0.9.0",
+      "com.lihaoyi" %% "ujson" % "4.0.0",
+      "com.lihaoyi" %% "scalatags" % "0.12.0",
+      "com.lihaoyi" %% "os-lib" % "0.10.3",
+      "com.lihaoyi" %% "mainargs" % "0.7.5",
       "org.lz4" % "lz4-java" % "1.8.0",
-      "org.json" % "json" % "20211205",
-      "org.scala-lang.modules" %% "scala-collection-compat" % "2.4.0",
-      "org.tukaani" % "xz" % "1.8",
-      "org.yaml" % "snakeyaml" % "1.30",
+      "org.json" % "json" % "20240303",
+      "org.scala-lang.modules" %% "scala-collection-compat" % "2.12.0",
+      "org.tukaani" % "xz" % "1.9",
+      "org.yaml" % "snakeyaml" % "2.0",
     ),
     libraryDependencies ++= Seq(
-      "com.lihaoyi" %% "utest" % "0.7.7",
+      "com.lihaoyi" %% "utest" % "0.8.3",
     ).map(_ % "test"),
     testFrameworks += new TestFramework("utest.runner.Framework"),
     (Compile / unmanagedSourceDirectories) := Seq(
@@ -56,4 +82,12 @@ lazy val bench = (project in file("bench"))
   .enablePlugins(JmhPlugin)
   .settings(
     run / fork := true,
+    // Do not cross-compile the benchmark
+    scalaVersion := scala3,
   )
+
+lazy val root = (project in file("."))
+  .aggregate(main)
+  .settings(
+    publishArtifact := false
+  )
\ No newline at end of file
diff --git a/project/plugins.sbt b/project/plugins.sbt
index b0a3453b..81e64e63 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -1,2 +1 @@
-//addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.7")
-addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.3")
+addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.3")
\ No newline at end of file
diff --git a/sjsonnet/src-jvm-native/sjsonnet/CachedResolvedFile.scala b/sjsonnet/src-jvm-native/sjsonnet/CachedResolvedFile.scala
index a5c93946..549ee339 100644
--- a/sjsonnet/src-jvm-native/sjsonnet/CachedResolvedFile.scala
+++ b/sjsonnet/src-jvm-native/sjsonnet/CachedResolvedFile.scala
@@ -26,7 +26,7 @@ class CachedResolvedFile(val resolvedImportPath: OsPath, memoryLimitBytes: Long,
   // Assert that the file is less than limit
   assert(jFile.length() <= memoryLimitBytes, s"Resolved import path ${resolvedImportPath} is too large: ${jFile.length()} bytes > ${memoryLimitBytes} bytes")
 
-  private[this] val resolvedImportContent: StaticResolvedFile = {
+  private val resolvedImportContent: StaticResolvedFile = {
     if (jFile.length() > cacheThresholdBytes) {
       // If the file is too large, then we will just read it from disk
       null
@@ -35,7 +35,7 @@ class CachedResolvedFile(val resolvedImportPath: OsPath, memoryLimitBytes: Long,
     }
   }
 
-  private[this] def readString(jFile: File): String = {
+  private def readString(jFile: File): String = {
     new String(Files.readAllBytes(jFile.toPath), StandardCharsets.UTF_8);
   }
 
diff --git a/sjsonnet/src-jvm-native/sjsonnet/OsPath.scala b/sjsonnet/src-jvm-native/sjsonnet/OsPath.scala
index 91827ef0..996e00c5 100644
--- a/sjsonnet/src-jvm-native/sjsonnet/OsPath.scala
+++ b/sjsonnet/src-jvm-native/sjsonnet/OsPath.scala
@@ -29,6 +29,6 @@ case class OsPath(p: os.Path) extends Path{
         ":" + Util.prettyIndex(lineStarts, offset)
       }
 
-    p.relativeTo(os.pwd) + offsetStr
+    p.relativeTo(os.pwd).toString() + offsetStr
   }
 }
\ No newline at end of file
diff --git a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala
index 6a59856a..c5d5b050 100644
--- a/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala
+++ b/sjsonnet/src-jvm-native/sjsonnet/SjsonnetMain.scala
@@ -64,7 +64,7 @@ object SjsonnetMain {
     val doc = "usage: sjsonnet  [sjsonnet-options] script-file"
     val result = for{
       config <- parser.constructEither(
-        args,
+        args.toIndexedSeq,
         customName = name, customDoc = doc,
         autoPrintHelpAndExit = None
       )
@@ -113,7 +113,7 @@ object SjsonnetMain {
   def writeFile(config: Config, f: os.Path, contents: String): Either[String, Unit] =
     handleWriteFile(os.write.over(f, contents, createFolders = config.createDirs.value))
 
-  def writeToFile(config: Config, wd: os.Path)(materialize: Writer => Either[String, _]): Either[String, String] = {
+  def writeToFile(config: Config, wd: os.Path)(materialize: Writer => Either[String, ?]): Either[String, String] = {
     config.outputFile match{
       case None =>
         val sw = new StringWriter
@@ -205,12 +205,12 @@ object SjsonnetMain {
       importer = importer match{
         case Some(i) => new Importer {
           def resolve(docBase: Path, importName: String): Option[Path] =
-            i(docBase, importName).map(OsPath)
+            i(docBase, importName).map(OsPath.apply)
           def read(path: Path): Option[ResolvedFile] = {
             readPath(path)
           }
         }
-        case None => resolveImport(config.jpaths.map(os.Path(_, wd)).map(OsPath(_)), allowedInputs)
+        case None => resolveImport(config.jpaths.map(os.Path(_, wd)).map(OsPath.apply), allowedInputs)
       },
       parseCache,
       settings = new Settings(
@@ -246,8 +246,8 @@ object SjsonnetMain {
                       Right(writer.toString)
                     }
                   }
-                  relPath = os.FilePath(multiPath) / os.RelPath(f)
-                  _ <- writeFile(config, relPath.resolveFrom(wd), rendered)
+                  relPath = os.Path(multiPath, wd) / f
+                  _ <- writeFile(config, relPath, rendered)
                 } yield relPath
               }
 
@@ -299,7 +299,7 @@ object SjsonnetMain {
    * of caching on top of the underlying file system. Small files are read into memory, while large
    * files are read from disk.
    */
-  private[this] def readPath(path: Path): Option[ResolvedFile] = {
+  private def readPath(path: Path): Option[ResolvedFile] = {
     val osPath = path.asInstanceOf[OsPath].p
     if (os.exists(osPath) && os.isFile(osPath)) {
       Some(new CachedResolvedFile(path.asInstanceOf[OsPath], memoryLimitBytes = Int.MaxValue.toLong))
diff --git a/sjsonnet/src-jvm/sjsonnet/Platform.scala b/sjsonnet/src-jvm/sjsonnet/Platform.scala
index c56af2a5..a1ed1336 100644
--- a/sjsonnet/src-jvm/sjsonnet/Platform.scala
+++ b/sjsonnet/src-jvm/sjsonnet/Platform.scala
@@ -2,13 +2,13 @@ package sjsonnet
 
 import org.json.JSONObject
 
-import java.io.{ByteArrayOutputStream, BufferedInputStream, File, FileInputStream}
+import java.io.{BufferedInputStream, ByteArrayOutputStream, File, FileInputStream}
 import java.util.Base64
 import java.util.zip.GZIPOutputStream
-import net.jpountz.xxhash.{StreamingXXHash64, XXHashFactory, XXHash64}
+import net.jpountz.xxhash.{StreamingXXHash64, XXHash64, XXHashFactory}
 import org.tukaani.xz.LZMA2Options
 import org.tukaani.xz.XZOutputStream
-import org.yaml.snakeyaml.Yaml
+import org.yaml.snakeyaml.{LoaderOptions, Yaml}
 import org.yaml.snakeyaml.constructor.Constructor
 
 object Platform {
@@ -21,12 +21,13 @@ object Platform {
     outputStream.close()
     gzippedBase64
   }
+
   def gzipString(s: String): String = {
     gzipBytes(s.getBytes())
   }
 
   /**
-   *  Valid compression levels are 0 (no compression) to 9 (maximum compression).
+   * Valid compression levels are 0 (no compression) to 9 (maximum compression).
    */
   def xzBytes(b: Array[Byte], compressionLevel: Option[Int]): String = {
     val outputStream: ByteArrayOutputStream = new ByteArrayOutputStream(b.length)
@@ -45,17 +46,19 @@ object Platform {
   }
 
   def yamlToJson(yamlString: String): String = {
-    val yaml: java.util.LinkedHashMap[String, Object] = new Yaml(new Constructor(classOf[java.util.LinkedHashMap[String, Object]])).load(yamlString)
+    val options = new LoaderOptions()
+    val yaml: java.util.LinkedHashMap[String, Object] = new Yaml(new Constructor(classOf[java.util.LinkedHashMap[String, Object]], options)).load(yamlString)
     new JSONObject(yaml).toString()
   }
+
   def md5(s: String): String = {
     java.security.MessageDigest.getInstance("MD5")
       .digest(s.getBytes("UTF-8"))
-      .map{ b => String.format("%02x", new java.lang.Integer(b & 0xff))}
+      .map { b => String.format("%02x", Integer.valueOf(b & 0xff)) }
       .mkString
   }
 
-  private[this] val xxHashFactory = XXHashFactory.fastestInstance()
+  private val xxHashFactory = XXHashFactory.fastestInstance()
 
   def hashFile(file: File): String = {
     val buffer = new Array[Byte](8192)
diff --git a/sjsonnet/src/sjsonnet/BaseCharRenderer.scala b/sjsonnet/src/sjsonnet/BaseCharRenderer.scala
index 406ceb64..5cec5045 100644
--- a/sjsonnet/src/sjsonnet/BaseCharRenderer.scala
+++ b/sjsonnet/src/sjsonnet/BaseCharRenderer.scala
@@ -10,16 +10,16 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output]
 (out: T,
  indent: Int = -1,
  escapeUnicode: Boolean = false) extends JsVisitor[T, T]{
-  protected[this] val elemBuilder = new upickle.core.CharBuilder
-  protected[this] val unicodeCharBuilder = new upickle.core.CharBuilder()
+  protected val elemBuilder = new upickle.core.CharBuilder
+  protected val unicodeCharBuilder = new upickle.core.CharBuilder()
   def flushCharBuilder() = {
     elemBuilder.writeOutToIfLongerThan(out, if (depth == 0) 0 else 1000)
   }
 
-  protected[this] var depth: Int = 0
+  protected var depth: Int = 0
 
 
-  protected[this] var commaBuffered = false
+  protected var commaBuffered = false
 
   def flushBuffer() = {
     if (commaBuffered) {
@@ -49,7 +49,7 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output]
     }
   }
 
-  def visitObject(length: Int, index: Int) = new ObjVisitor[T, T] {
+  def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[T, T] {
     flushBuffer()
     elemBuilder.append('{')
     depth += 1
@@ -144,7 +144,7 @@ class BaseCharRenderer[T <: upickle.core.CharOps.Output]
 
   def visitNonNullString(s: CharSequence, index: Int) = {
     flushBuffer()
-    upickle.core.RenderUtils.escapeChar(unicodeCharBuilder, elemBuilder, s, escapeUnicode)
+    upickle.core.RenderUtils.escapeChar(unicodeCharBuilder, elemBuilder, s, escapeUnicode, wrapQuotes = true)
     flushCharBuilder()
     out
   }
diff --git a/sjsonnet/src/sjsonnet/BaseRenderer.scala b/sjsonnet/src/sjsonnet/BaseRenderer.scala
index 3bd2a632..dca22588 100644
--- a/sjsonnet/src/sjsonnet/BaseRenderer.scala
+++ b/sjsonnet/src/sjsonnet/BaseRenderer.scala
@@ -47,7 +47,7 @@ class BaseRenderer[T <: java.io.Writer]
     }
   }
 
-  def visitObject(length: Int, index: Int) = new ObjVisitor[T, T] {
+  def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[T, T] {
     flushBuffer()
     out.append('{')
     depth += 1
diff --git a/sjsonnet/src/sjsonnet/DecimalFormat.scala b/sjsonnet/src/sjsonnet/DecimalFormat.scala
index 0e0566d3..9e0653df 100644
--- a/sjsonnet/src/sjsonnet/DecimalFormat.scala
+++ b/sjsonnet/src/sjsonnet/DecimalFormat.scala
@@ -22,7 +22,7 @@ object DecimalFormat {
   def leftPad(n: Long, targetWidth: Int): String = {
     val sign = if (n < 0) "-" else ""
     val absN = math.abs(n)
-    val nWidth = if (absN == 0) 1 else Math.log10(absN).toInt + 1
+    val nWidth = if (absN == 0) 1 else Math.log10(absN.toDouble).toInt + 1
     sign + "0" * (targetWidth - nWidth) + absN
   }
   def rightPad(n0: Long, minWidth: Int, maxWidth: Int): String = {
@@ -31,7 +31,7 @@ object DecimalFormat {
       val n = (n0 / Math.pow(10, trailingZeroes(n0))).toInt
       assert(n == math.abs(n))
       val nWidth = if (n == 0) 1 else Math.log10(n).toInt + 1
-      (n + "0" * (minWidth - nWidth)).take(maxWidth)
+      (n.toString + "0" * (minWidth - nWidth)).take(maxWidth)
     }
   }
   def format(fracLengthOpt: Option[(Int, Int)], expLengthOpt: Option[Int], number: Double): String = {
@@ -39,13 +39,13 @@ object DecimalFormat {
       case Some(expLength) =>
         val roundLog10 = Math.ceil(Math.log10(Math.abs(number))).toLong
         val expNum = roundLog10 - 1
-        val scaled = number / math.pow(10, expNum)
+        val scaled = number / math.pow(10, expNum.toDouble)
         val prefix = scaled.toLong.toString
         val expFrag = leftPad(expNum, expLength)
         val fracFrag = fracLengthOpt.map{case (zeroes, hashes) =>
           if (zeroes == 0 && hashes == 0) ""
           else {
-            val divided = number / Math.pow(10, expNum - zeroes - hashes)
+            val divided = number / Math.pow(10, (expNum - zeroes - hashes).toDouble)
             val scaledFrac = divided % Math.pow(10, zeroes + hashes)
             rightPad(Math.abs(Math.round(scaledFrac)), zeroes, zeroes + hashes)
           }
diff --git a/sjsonnet/src/sjsonnet/Error.scala b/sjsonnet/src/sjsonnet/Error.scala
index edbe18d2..f080f7d2 100644
--- a/sjsonnet/src/sjsonnet/Error.scala
+++ b/sjsonnet/src/sjsonnet/Error.scala
@@ -32,11 +32,11 @@ class Error(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Thro
   def asSeenFrom(ev: EvalErrorScope): Error =
     copy(stack = stack.map(_.asSeenFrom(ev)))
 
-  protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
+  protected def copy(msg: String = msg, stack: List[Error.Frame] = stack,
                            underlying: Option[Throwable] = underlying) =
     new Error(msg, stack, underlying)
 
-  private[this] def alwaysAddPos(expr: Expr): Boolean = expr match {
+  private def alwaysAddPos(expr: Expr): Boolean = expr match {
     case _: Expr.LocalExpr | _: Expr.Arr | _: Expr.ObjExtend | _: Expr.ObjBody | _: Expr.IfElse => false
     case _ => true
   }
@@ -77,7 +77,7 @@ object Error {
 class ParseError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
   extends Error(msg, stack, underlying) {
 
-  override protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
+  override protected def copy(msg: String = msg, stack: List[Error.Frame] = stack,
                                     underlying: Option[Throwable] = underlying) =
     new ParseError(msg, stack, underlying)
 }
@@ -85,7 +85,7 @@ class ParseError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option
 class StaticError(msg: String, stack: List[Error.Frame] = Nil, underlying: Option[Throwable] = None)
   extends Error(msg, stack, underlying) {
 
-  override protected[this] def copy(msg: String = msg, stack: List[Error.Frame] = stack,
+  override protected def copy(msg: String = msg, stack: List[Error.Frame] = stack,
                                     underlying: Option[Throwable] = underlying) =
     new StaticError(msg, stack, underlying)
 }
diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala
index d9f1d1ea..28550791 100644
--- a/sjsonnet/src/sjsonnet/Evaluator.scala
+++ b/sjsonnet/src/sjsonnet/Evaluator.scala
@@ -170,7 +170,7 @@ class Evaluator(resolver: CachedResolver,
         case _ => fail()
       }
       case Expr.UnaryOp.OP_~ => v match {
-        case Val.Num(_, v) => Val.Num(pos, ~v.toLong)
+        case Val.Num(_, v) => Val.Num(pos, (~v.toLong).toDouble)
         case _ => fail()
       }
       case Expr.UnaryOp.OP_+ => v match {
@@ -430,12 +430,12 @@ class Evaluator(resolver: CachedResolver,
       }
 
       case Expr.BinaryOp.OP_<< => (l, r) match {
-        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l.toLong << r.toLong)
+        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong << r.toLong).toDouble)
         case _ => fail()
       }
 
       case Expr.BinaryOp.OP_>> => (l, r) match {
-        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l.toLong >> r.toLong)
+        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong >> r.toLong).toDouble)
         case _ => fail()
       }
 
@@ -445,17 +445,17 @@ class Evaluator(resolver: CachedResolver,
       }
 
       case Expr.BinaryOp.OP_& => (l, r) match {
-        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l.toLong & r.toLong)
+        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong & r.toLong).toDouble)
         case _ => fail()
       }
 
       case Expr.BinaryOp.OP_^ => (l, r) match {
-        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l.toLong ^ r.toLong)
+        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong ^ r.toLong).toDouble)
         case _ => fail()
       }
 
       case Expr.BinaryOp.OP_| => (l, r) match {
-        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, l.toLong | r.toLong)
+        case (Val.Num(_, l), Val.Num(_, r)) => Val.Num(pos, (l.toLong | r.toLong).toDouble)
         case _ => fail()
       }
 
@@ -502,14 +502,20 @@ class Evaluator(resolver: CachedResolver,
   def visitMemberList(objPos: Position, e: ObjBody.MemberList, sup: Val.Obj)(implicit scope: ValScope): Val.Obj = {
     val asserts = e.asserts
     val fields = e.fields
-    var cachedSimpleScope: ValScope = null.asInstanceOf[ValScope]
+    var cachedSimpleScope: ValScope | Null = null
     var cachedObj: Val.Obj = null
     var asserting: Boolean = false
 
     def makeNewScope(self: Val.Obj, sup: Val.Obj): ValScope = {
       if((sup eq null) && (self eq cachedObj)) {
-        if(cachedSimpleScope == null.asInstanceOf[ValScope]) cachedSimpleScope = createNewScope(self, sup)
-        cachedSimpleScope
+        cachedSimpleScope match {
+          case null =>
+            val cachedScope = createNewScope(self, sup)
+            cachedSimpleScope = cachedScope
+            cachedScope
+          case cachedScope: ValScope =>
+            cachedScope
+        }
       } else createNewScope(self, sup)
     }
 
@@ -581,7 +587,8 @@ class Evaluator(resolver: CachedResolver,
           val v = new Val.Obj.Member(plus, sep) {
             def invoke(self: Val.Obj, sup: Val.Obj, fs: FileScope, ev: EvalScope): Val = {
               if(asserts != null) assertions(self)
-              visitExpr(rhs)(makeNewScope(self, sup))
+              val newScope = makeNewScope(self, sup)
+              visitExpr(rhs)(using newScope)
             }
           }
           builder.put(k, v)
diff --git a/sjsonnet/src/sjsonnet/Expr.scala b/sjsonnet/src/sjsonnet/Expr.scala
index 5e3f371f..a475e4c8 100644
--- a/sjsonnet/src/sjsonnet/Expr.scala
+++ b/sjsonnet/src/sjsonnet/Expr.scala
@@ -21,7 +21,7 @@ trait Expr{
   }
 }
 object Expr{
-  private final def arrStr(a: Array[_]): String = {
+  private final def arrStr(a: Array[?]): String = {
     if(a == null) "null" else a.mkString("[", ", ", "]")
   }
 
@@ -149,7 +149,7 @@ object Expr{
                    end: Option[Expr],
                    stride: Option[Expr]) extends Expr
   case class Function(pos: Position, params: Params, body: Expr) extends Expr
-  case class IfElse(pos: Position, cond: Expr, then: Expr, `else`: Expr) extends Expr
+  case class IfElse(pos: Position, cond: Expr, `then`: Expr, `else`: Expr) extends Expr
 
   sealed trait CompSpec extends Expr
   case class IfSpec(pos: Position, cond: Expr) extends CompSpec
diff --git a/sjsonnet/src/sjsonnet/ExprTransform.scala b/sjsonnet/src/sjsonnet/ExprTransform.scala
index 964833ee..970c5618 100644
--- a/sjsonnet/src/sjsonnet/ExprTransform.scala
+++ b/sjsonnet/src/sjsonnet/ExprTransform.scala
@@ -188,10 +188,10 @@ abstract class ExprTransform {
     }
   }
 
-  protected[this] def transformArr[T <: Expr](a: Array[T]): Array[T] =
-    transformGenericArr(a)((transform _).asInstanceOf[T => T])
+  protected def transformArr[T <: Expr](a: Array[T]): Array[T] =
+    transformGenericArr(a)((transform).asInstanceOf[T => T])
 
-  protected[this] def transformParams(p: Params): Params = {
+  protected def transformParams(p: Params): Params = {
     if(p == null) return null
     val defs = p.defaultExprs
     if(defs == null) p
@@ -202,16 +202,16 @@ abstract class ExprTransform {
     }
   }
 
-  protected[this] def transformBinds(a: Array[Bind]): Array[Bind] =
+  protected def transformBinds(a: Array[Bind]): Array[Bind] =
     transformGenericArr(a)(transformBind)
 
-  protected[this] def transformFields(a: Array[Member.Field]): Array[Member.Field] =
+  protected def transformFields(a: Array[Member.Field]): Array[Member.Field] =
     transformGenericArr(a)(transformField)
 
-  protected[this] def transformAsserts(a: Array[Member.AssertStmt]): Array[Member.AssertStmt] =
+  protected def transformAsserts(a: Array[Member.AssertStmt]): Array[Member.AssertStmt] =
     transformGenericArr(a)(transformAssert)
 
-  protected[this] def transformBind(b: Bind): Bind = {
+  protected def transformBind(b: Bind): Bind = {
     val args = b.args
     val rhs = b.rhs
     val args2 = transformParams(args)
@@ -220,7 +220,7 @@ abstract class ExprTransform {
     else b.copy(args = args2, rhs = rhs2)
   }
 
-  protected[this] def transformField(f: Member.Field): Member.Field = {
+  protected def transformField(f: Member.Field): Member.Field = {
     val x = f.fieldName
     val y = f.args
     val z = f.rhs
@@ -231,14 +231,14 @@ abstract class ExprTransform {
     else f.copy(fieldName = x2, args = y2, rhs = z2)
   }
 
-  protected[this] def transformFieldName(f: FieldName): FieldName = f match {
+  protected def transformFieldName(f: FieldName): FieldName = f match {
     case FieldName.Dyn(x) =>
       val x2 = transform(x)
       if(x2 eq x) f else FieldName.Dyn(x2)
     case _ => f
   }
 
-  protected[this] def transformAssert(a: Member.AssertStmt): Member.AssertStmt = {
+  protected def transformAssert(a: Member.AssertStmt): Member.AssertStmt = {
     val x = a.value
     val y = a.msg
     val x2 = transform(x)
@@ -247,14 +247,14 @@ abstract class ExprTransform {
     else a.copy(value = x2, msg = y2)
   }
 
-  protected[this] def transformOption(o: Option[Expr]): Option[Expr] = o match {
+  protected def transformOption(o: Option[Expr]): Option[Expr] = o match {
     case Some(e) =>
       val e2 = transform(e)
       if(e2 eq e) o else Some(e2)
     case None => o
   }
 
-  protected[this] def transformList(l: List[Expr]): List[Expr] = {
+  protected def transformList(l: List[Expr]): List[Expr] = {
     val lb = List.newBuilder[Expr]
     var diff = false
     l.foreach { e =>
@@ -265,7 +265,7 @@ abstract class ExprTransform {
     if(diff) lb.result() else l
   }
 
-  protected[this] def transformGenericArr[T <: AnyRef](a: Array[T])(f: T => T): Array[T] = {
+  protected def transformGenericArr[T <: AnyRef](a: Array[T])(f: T => T): Array[T] = {
     if(a == null) return null
     var i = 0
     while(i < a.length) {
diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala
index e9bfe8e6..6d742974 100644
--- a/sjsonnet/src/sjsonnet/Format.scala
+++ b/sjsonnet/src/sjsonnet/Format.scala
@@ -21,14 +21,15 @@ object Format{
                         width: Option[Int],
                         precision: Option[Int],
                         conversion: Char)
-  import fastparse._, NoWhitespace._
-  def integer[_: P]           = P( CharIn("1-9") ~ CharsWhileIn("0-9", 0) | "0" )
-  def label[_: P] = P( ("(" ~ CharsWhile(_ != ')').! ~ ")").? )
-  def flags[_: P] = P( CharsWhileIn("#0\\- +", 0).! )
-  def width[_: P] = P( (integer | "*").!.? )
-  def precision[_: P] = P( ("." ~/ integer.!).? )
-  def conversion[_: P] = P( CharIn("diouxXeEfFgGcrsa%").! )
-  def formatSpec[_: P] = P( label ~ flags ~ width ~ precision ~ CharIn("hlL").? ~ conversion ).map{
+  import fastparse.*, NoWhitespace.*
+
+  def integer[A: P]           = P( CharIn("1-9") ~ CharsWhileIn("0-9", 0) | "0" )
+  def label[A: P] = P( ("(" ~ CharsWhile(_ != ')').! ~ ")").? )
+  def flags[A: P] = P( CharsWhileIn("#0\\- +", 0).! )
+  def width[A: P] = P( (integer | "*").!.? )
+  def precision[A: P] = P( ("." ~/ integer.!).? )
+  def conversion[A: P] = P( CharIn("diouxXeEfFgGcrsa%").! )
+  def formatSpec[A: P] = P( label ~ flags ~ width ~ precision ~ CharIn("hlL").? ~ conversion ).map{
     case (label, flags, width, precision, conversion) =>
       FormatSpec(
         label,
@@ -44,8 +45,8 @@ object Format{
   }
 
 
-  def plain[_: P] = P( CharsWhile(_ != '%', 0).! )
-  def format[_: P] = P( plain ~ (("%" ~/ formatSpec) ~ plain).rep ~ End)
+  def plain[A: P] = P( CharsWhile(_ != '%', 0).! )
+  def format[A: P] = P( plain ~ (("%" ~/ formatSpec) ~ plain).rep ~ End)
 
 
 
@@ -80,7 +81,7 @@ object Format{
              values0: Val,
              pos: Position)
             (implicit evaluator: EvalScope): String = {
-    val (leading, chunks) = fastparse.parse(s, format(_)).get.value
+    val (leading, chunks) = fastparse.parse(s, format).get.value
     format(leading, chunks, values0, pos)
   }
 
@@ -256,7 +257,7 @@ object Format{
   }
 
   class PartialApplyFmt(fmt: String) extends Val.Builtin1("values") {
-    val (leading, chunks) = fastparse.parse(fmt, format(_)).get.value
+    val (leading, chunks) = fastparse.parse(fmt, format).get.value
     def evalRhs(values0: Val, ev: EvalScope, pos: Position): Val =
       Val.Str(pos, format(leading, chunks, values0, pos)(ev))
   }
diff --git a/sjsonnet/src/sjsonnet/Importer.scala b/sjsonnet/src/sjsonnet/Importer.scala
index c4e616a2..b42c3f66 100644
--- a/sjsonnet/src/sjsonnet/Importer.scala
+++ b/sjsonnet/src/sjsonnet/Importer.scala
@@ -33,7 +33,7 @@ object Importer {
 
 case class FileParserInput(file: File) extends ParserInput {
 
-  private[this] val bufferedFile = new BufferedRandomAccessFile(file.getAbsolutePath, 1024 * 8)
+  private val bufferedFile = new BufferedRandomAccessFile(file.getAbsolutePath, 1024 * 8)
 
   private lazy val fileLength = file.length.toInt
 
@@ -55,7 +55,7 @@ case class FileParserInput(file: File) extends ParserInput {
 
   override def checkTraceable(): Unit = {}
 
-  private[this] lazy val lineNumberLookup: Array[Int] = {
+  private lazy val lineNumberLookup: Array[Int] = {
     val lines = mutable.ArrayBuffer[Int]()
     val bufferedStream = new BufferedInputStream(new FileInputStream(file))
     var byteRead: Int = 0
@@ -149,7 +149,7 @@ trait ResolvedFile {
   def readString(): String
 
   // Get a content hash of the file suitable for detecting changes in a given file.
-  def contentHash(): String
+  def contentHash: String
 }
 
 case class StaticResolvedFile(content: String) extends ResolvedFile {
@@ -158,7 +158,7 @@ case class StaticResolvedFile(content: String) extends ResolvedFile {
   def readString(): String = content
 
   // We just cheat, the content hash can be the content itself for static imports
-  lazy val contentHash: String = content
+  def contentHash: String = content
 }
 
 class CachedImporter(parent: Importer) extends Importer {
@@ -184,8 +184,9 @@ class CachedResolver(
   internedStaticFieldSets: mutable.HashMap[Val.StaticObjectFieldSet, java.util.LinkedHashMap[String, java.lang.Boolean]]) extends CachedImporter(parentImporter) {
 
   def parse(path: Path, content: ResolvedFile)(implicit ev: EvalErrorScope): Either[Error, (Expr, FileScope)] = {
-    parseCache.getOrElseUpdate((path, content.contentHash.toString), {
-      val parsed = fastparse.parse(content.getParserInput(), new Parser(path, strictImportSyntax, internedStrings, internedStaticFieldSets).document(_)) match {
+    parseCache.getOrElseUpdate((path, content.contentHash), {
+      val parser = new Parser(path, strictImportSyntax, internedStrings, internedStaticFieldSets)
+      val parsed = fastparse.parse(content.getParserInput(), parser.document(using _)) match {
         case f @ Parsed.Failure(_, _, _) =>
           val traced = f.trace()
           val pos = new Position(new FileScope(path), traced.index)
diff --git a/sjsonnet/src/sjsonnet/Interpreter.scala b/sjsonnet/src/sjsonnet/Interpreter.scala
index 3de38fa6..74e0afb9 100644
--- a/sjsonnet/src/sjsonnet/Interpreter.scala
+++ b/sjsonnet/src/sjsonnet/Interpreter.scala
@@ -128,6 +128,6 @@ class Interpreter(extVars: Map[String, String],
           )
         }
       }
-    handleException(m.apply0(res, visitor)(evaluator))
+    handleException(m.apply0(res, visitor)(using evaluator))
   }
 }
diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala
index c6bea8f5..747dcd5f 100644
--- a/sjsonnet/src/sjsonnet/Materializer.scala
+++ b/sjsonnet/src/sjsonnet/Materializer.scala
@@ -25,7 +25,7 @@ abstract class Materializer {
       case obj: Val.Obj =>
         storePos(obj.pos)
         obj.triggerAllAsserts(obj)
-        val objVisitor = visitor.visitObject(obj.visibleKeyNames.length , -1)
+        val objVisitor = visitor.visitObject(obj.visibleKeyNames.length, jsonableKeys = true, -1)
         val sort = !evaluator.settings.preserveOrder
         var prevKey: String = null
         obj.foreachElement(sort, evaluator.emptyMaterializeFileScopePos) { (k, v) =>
diff --git a/sjsonnet/src/sjsonnet/Parser.scala b/sjsonnet/src/sjsonnet/Parser.scala
index 3db4fb85..1d728a5d 100644
--- a/sjsonnet/src/sjsonnet/Parser.scala
+++ b/sjsonnet/src/sjsonnet/Parser.scala
@@ -1,7 +1,7 @@
 package sjsonnet
 
-import fastparse.JsonnetWhitespace._
-import fastparse._
+import fastparse.JsonnetWhitespace.*
+import fastparse.*
 import Expr.Member.Visibility
 
 import scala.annotation.switch
@@ -48,19 +48,19 @@ class Parser(val currentFile: Path,
              strictImportSyntax: Boolean,
              internedStrings: mutable.HashMap[String, String],
              internedStaticFieldSets: mutable.HashMap[Val.StaticObjectFieldSet, java.util.LinkedHashMap[String, java.lang.Boolean]]) {
-  import Parser._
+  import Parser.*
 
   private val fileScope = new FileScope(currentFile)
 
-  def Pos[_: P]: P[Position] = Index.map(offset => new Position(fileScope, offset))
+  def Pos[A: P]: P[Position] = Index.map(offset => new Position(fileScope, offset))
 
-  def id[_: P] = P(
+  def id[A: P] = P(
     CharIn("_a-zA-Z") ~~
     CharsWhileIn("_a-zA-Z0-9", 0)
   ).!.filter(s => !keywords.contains(s))
 
-  def break[_: P] = P(!CharIn("_a-zA-Z0-9"))
-  def number[_: P]: P[Val.Num] = P(
+  def break[A: P] = P(!CharIn("_a-zA-Z0-9"))
+  def number[A: P]: P[Val.Num] = P(
     Pos ~~ (
       CharsWhileIn("0-9") ~~
       ("." ~ CharsWhileIn("0-9")).? ~~
@@ -68,8 +68,8 @@ class Parser(val currentFile: Path,
     ).!
   ).map(s => Val.Num(s._1, s._2.toDouble))
 
-  def escape[_: P] = P( escape0 | escape1 )
-  def escape0[_: P] = P("\\" ~~ !"u" ~~ AnyChar.!).map{
+  def escape[A: P] = P( escape0 | escape1 )
+  def escape0[A: P] = P("\\" ~~ !"u" ~~ AnyChar.!).map{
     case "\"" => "\""
     case "'" => "\'"
     case "\\" => "\\"
@@ -80,29 +80,29 @@ class Parser(val currentFile: Path,
     case "r" => "\r"
     case "t" => "\t"
   }
-  def escape1[_: P] = P( "\\u" ~~ CharIn("0-9a-fA-F").repX(min=4, max=4).! ).map{
+  def escape1[A: P] = P( "\\u" ~~ CharIn("0-9a-fA-F").repX(min=4, max=4).! ).map{
     s => Integer.parseInt(s, 16).toChar.toString
   }
-  def doubleString[_: P]: P[Seq[String]] =
+  def doubleString[A: P]: P[Seq[String]] =
     P( (CharsWhile(x => x != '"' && x != '\\').! | escape).repX ~~ "\"" )
-  def singleString[_: P]: P[Seq[String]] =
+  def singleString[A: P]: P[Seq[String]] =
     P( (CharsWhile(x => x != '\'' && x != '\\').! | escape).repX ~~ "'" )
-  def literalDoubleString[_: P]: P[Seq[String]] =
+  def literalDoubleString[A: P]: P[Seq[String]] =
     P( (CharsWhile(_ != '"').! | "\"\"".!.map(_ => "\"")).repX ~~ "\""  )
-  def literalSingleString[_: P]: P[Seq[String]] =
+  def literalSingleString[A: P]: P[Seq[String]] =
     P( (CharsWhile(_ != '\'').! | "''".!.map(_ => "'")).repX ~~ "'" )
 
-  def tripleBarStringLines[_: P]: P[Seq[String]] = P(
+  def tripleBarStringLines[A: P]: P[Seq[String]] = P(
     tripleBarStringHead.flatMapX { case (pre, w, head) =>
       tripleBarStringBody(w).map(pre ++ Seq(head, "\n") ++ _)
     }
   )
-  def tripleBarString[_: P]: P[Seq[String]] = P(
+  def tripleBarString[A: P]: P[Seq[String]] = P(
     "||"./ ~~ CharsWhileIn(" \t", 0) ~~
     "\n" ~~ tripleBarStringLines ~~ "\n" ~~
     CharsWhileIn(" \t", 0) ~~ "|||"
   )
-  def string[_: P]: P[String] = P(
+  def string[A: P]: P[String] = P(
     SingleChar.flatMapX{
       case '\"' => doubleString
       case '\'' => singleString
@@ -116,24 +116,24 @@ class Parser(val currentFile: Path,
     }
   ).map(_.mkString)
 
-  def tripleBarStringHead[_: P] = P(
+  def tripleBarStringHead[A: P] = P(
     (CharsWhileIn(" \t", 0) ~~ "\n".!).repX ~~
       CharsWhileIn(" \t", 1).! ~~
       CharsWhile(_ != '\n').!
   )
-  def tripleBarBlankHead[_: P]: P[String] =
+  def tripleBarBlankHead[A: P]: P[String] =
     P( CharsWhileIn(" \t", 0) ~~ &("\n").map(_ => "\n") )
 
-  def tripleBarBlank[_: P]: P[String] = P( "\n" ~~ tripleBarBlankHead )
+  def tripleBarBlank[A: P]: P[String] = P( "\n" ~~ tripleBarBlankHead )
 
-  def tripleBarStringBody[_: P](w: String): P[Seq[String]] = P(
+  def tripleBarStringBody[A: P](w: String): P[Seq[String]] = P(
     (tripleBarBlank | "\n" ~~ w ~~ CharsWhile(_ != '\n').!.map(_ + "\n")).repX
   )
 
 
-  def arr[_: P]: P[Expr] = P( (Pos ~~ &("]")).map(new Val.Arr(_, emptyLazyArray)) | arrBody )
-  def compSuffix[_: P] = P( forspec ~ compspec ).map(Left(_))
-  def arrBody[_: P]: P[Expr] = P(
+  def arr[A: P]: P[Expr] = P( (Pos ~~ &("]")).map(new Val.Arr(_, emptyLazyArray)) | arrBody )
+  def compSuffix[A: P] = P( forspec ~ compspec ).map(Left(_))
+  def arrBody[A: P]: P[Expr] = P(
     Pos ~~ expr ~
     (compSuffix | "," ~ (compSuffix | (expr.rep(0, sep = ",") ~ ",".?).map(Right(_)))).?
   ).map{
@@ -152,19 +152,19 @@ class Parser(val currentFile: Path,
     case (offset, first, Some(Right(rest))) => Expr.Arr(offset, Array(first) ++ rest)
   }
 
-  def assertExpr[_: P](pos: Position): P[Expr] =
+  def assertExpr[A: P](pos: Position): P[Expr] =
     P( assertStmt ~ ";" ~ expr ).map(t => Expr.AssertExpr(pos, t._1, t._2))
 
-  def function[_: P](pos: Position): P[Expr] =
+  def function[A: P](pos: Position): P[Expr] =
     P( "(" ~/ params ~ ")" ~ expr ).map(t => Expr.Function(pos, t._1, t._2))
 
-  def ifElse[_: P](pos: Position): P[Expr] =
-    P( Pos ~~ expr ~ "then" ~~ break ~ expr ~ ("else" ~~ break ~ expr).?.map(_.getOrElse(null)) ).map(Expr.IfElse.tupled)
+  def ifElse[A: P](pos: Position): P[Expr] =
+    P( Pos ~~ expr ~ "then" ~~ break ~ expr ~ ("else" ~~ break ~ expr).?.map(_.orNull) ).map((Expr.IfElse.apply).tupled)
 
-  def localExpr[_: P]: P[Expr] =
-    P( Pos ~~ bind.rep(min=1, sep = ","./).map(s => if(s.isEmpty) null else s.toArray) ~ ";" ~ expr ).map(Expr.LocalExpr.tupled)
+  def localExpr[A: P]: P[Expr] =
+    P( Pos ~~ bind.rep(min=1, sep = ","./).map(s => if(s.isEmpty) null else s.toArray) ~ ";" ~ expr ).map((Expr.LocalExpr.apply).tupled)
 
-  def expr[_: P]: P[Expr] =
+  def expr[A: P]: P[Expr] =
     P("" ~ expr1 ~ (Pos ~~ binaryop ~/ expr1).rep ~ "").map{ case (pre, fs) =>
       var remaining = fs
       def climb(minPrec: Int, current: Expr): Expr = {
@@ -214,11 +214,11 @@ class Parser(val currentFile: Path,
       climb(0, pre)
     }
 
-  def expr1[_: P]: P[Expr] = P(expr2 ~ exprSuffix2.rep).map{
+  def expr1[A: P]: P[Expr] = P(expr2 ~ exprSuffix2.rep).map{
     case (pre, fs) => fs.foldLeft(pre){case (p, f) => f(p) }
   }
 
-  def exprSuffix2[_: P]: P[Expr => Expr] = P(
+  def exprSuffix2[A: P]: P[Expr => Expr] = P(
     Pos.flatMapX{i =>
       CharIn(".[({")./.!.map(_(0)).flatMapX{ c =>
         (c: @switch) match{
@@ -237,12 +237,12 @@ class Parser(val currentFile: Path,
     }
   )
 
-  def local[_: P] = P( localExpr )
-  def importStr[_: P](pos: Position) = P( importExpr.map(Expr.ImportStr(pos, _)) )
-  def `import`[_: P](pos: Position) = P( importExpr.map(Expr.Import(pos, _)) )
-  def error[_: P](pos: Position) = P(expr.map(Expr.Error(pos, _)) )
+  def local[A: P] = P( localExpr )
+  def importStr[A: P](pos: Position) = P( importExpr.map(Expr.ImportStr(pos, _)) )
+  def `import`[A: P](pos: Position) = P( importExpr.map(Expr.Import(pos, _)) )
+  def error[A: P](pos: Position) = P(expr.map(Expr.Error(pos, _)) )
 
-  def importExpr[_: P]: P[String] = P(
+  def importExpr[A: P]: P[String] = P(
     if (!strictImportSyntax) string
     else expr.flatMap {
       case Val.Str(_, s) => Pass(s)
@@ -250,7 +250,7 @@ class Parser(val currentFile: Path,
     }
   )
 
-  def unaryOpExpr[_: P](pos: Position, op: Char) = P(
+  def unaryOpExpr[A: P](pos: Position, op: Char) = P(
     expr1.map{ e =>
       def k2 = op match{
         case '+' => Expr.UnaryOp.OP_+
@@ -269,7 +269,7 @@ class Parser(val currentFile: Path,
   }
 
   // Any `expr` that isn't naively left-recursive
-  def expr2[_: P]: P[Expr] = P(
+  def expr2[A: P]: P[Expr] = P(
     Pos.flatMapX{ pos =>
       SingleChar.flatMapX{ c =>
         (c: @switch) match {
@@ -289,7 +289,7 @@ class Parser(val currentFile: Path,
           case '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' =>
             P.current.index = pos.offset; number
           case x if idStartChar(x) => CharsWhileIn("_a-zA-Z0-9", 0).!.flatMapX { y =>
-            x + y match {
+            s"$x$y" match {
               case "null"      => Pass(Val.Null(pos))
               case "true"      => Pass(Val.True(pos))
               case "false"     => Pass(Val.False(pos))
@@ -311,7 +311,7 @@ class Parser(val currentFile: Path,
     }
   )
 
-  def objinside[_: P]: P[Expr.ObjBody] = P(
+  def objinside[A: P]: P[Expr.ObjBody] = P(
     Pos ~ member.rep(sep = ",") ~ ",".? ~ (forspec ~ compspec).?
   ).flatMap { case t @ (pos, exprs, _) =>
     val seen = collection.mutable.Set.empty[String]
@@ -345,10 +345,10 @@ class Parser(val currentFile: Path,
         exprs(preLocals.length)
       val postLocals = exprs.drop(preLocals.length+1).takeWhile(_.isInstanceOf[Expr.Bind])
         .map(_.asInstanceOf[Expr.Bind])
-      
-      /* 
+
+      /*
        * Prevent duplicate fields in list comprehension. See: https://github.com/databricks/sjsonnet/issues/99
-       * 
+       *
        * If comps._1 is a forspec with value greater than one lhs cannot be a Expr.Str
        * Otherwise the field value will be overriden by the multiple iterations of forspec
        */
@@ -362,35 +362,35 @@ class Parser(val currentFile: Path,
       Expr.ObjBody.ObjComp(pos, preLocals.toArray, lhs, rhs, plus, postLocals.toArray, comps._1, comps._2.toList)
   }
 
-  def member[_: P]: P[Expr.Member] = P( objlocal | "assert" ~~ break ~ assertStmt | field )
-  def field[_: P] = P(
+  def member[A: P]: P[Expr.Member] = P( objlocal | "assert" ~~ break ~ assertStmt | field )
+  def field[A: P] = P(
     (Pos ~~ fieldname ~/ "+".!.? ~ ("(" ~ params ~ ")").? ~ fieldKeySep ~/ expr).map{
       case (pos, name, plus, p, h2, e) =>
-        Expr.Member.Field(pos, name, plus.nonEmpty, p.getOrElse(null), h2, e)
+        Expr.Member.Field(pos, name, plus.nonEmpty, p.orNull, h2, e)
     }
   )
-  def fieldKeySep[_: P] = P( StringIn(":::", "::", ":") ).!.map{
+  def fieldKeySep[A: P] = P( StringIn(":::", "::", ":") ).!.map{
     case ":" => Visibility.Normal
     case "::" => Visibility.Hidden
     case ":::" => Visibility.Unhide
   }
-  def objlocal[_: P] = P( "local" ~~ break ~/ bind )
-  def compspec[_: P]: P[Seq[Expr.CompSpec]] = P( (forspec | ifspec).rep )
-  def forspec[_: P] =
-    P( Pos ~~ "for" ~~ break ~/ id ~ "in" ~~ break ~ expr ).map(Expr.ForSpec.tupled)
-  def ifspec[_: P] = P( Pos ~~ "if" ~~ break  ~/ expr ).map(Expr.IfSpec.tupled)
-  def fieldname[_: P] = P(
-    id.map(Expr.FieldName.Fixed) |
-    string.map(Expr.FieldName.Fixed) |
-    "[" ~ expr.map(Expr.FieldName.Dyn) ~ "]"
+  def objlocal[A: P] = P( "local" ~~ break ~/ bind )
+  def compspec[A: P]: P[Seq[Expr.CompSpec]] = P( (forspec | ifspec).rep )
+  def forspec[A: P] =
+    P( Pos ~~ "for" ~~ break ~/ id ~ "in" ~~ break ~ expr ).map((Expr.ForSpec.apply).tupled)
+  def ifspec[A: P] = P( Pos ~~ "if" ~~ break  ~/ expr ).map((Expr.IfSpec.apply).tupled)
+  def fieldname[A: P] = P(
+    id.map(Expr.FieldName.Fixed.apply) |
+    string.map(Expr.FieldName.Fixed.apply) |
+    "[" ~ expr.map(Expr.FieldName.Dyn.apply) ~ "]"
   )
-  def assertStmt[_: P] =
-    P( expr ~ (":" ~ expr).?.map(_.getOrElse(null)) ).map(Expr.Member.AssertStmt.tupled)
+  def assertStmt[A: P] =
+    P( expr ~ (":" ~ expr).?.map(_.orNull) ).map((Expr.Member.AssertStmt.apply).tupled)
 
-  def bind[_: P] =
-    P( Pos ~~ id ~ ("(" ~/ params.? ~ ")").?.map(_.flatten).map(_.getOrElse(null)) ~ "=" ~ expr ).map(Expr.Bind.tupled)
+  def bind[A: P] =
+    P( Pos ~~ id ~ ("(" ~/ params.? ~ ")").?.map(_.flatten).map(_.orNull) ~ "=" ~ expr ).map((Expr.Bind.apply).tupled)
 
-  def args[_: P] = P( ((id ~ "=" ~ !"=").? ~ expr).rep(sep = ",") ~ ",".? ).flatMapX{ x =>
+  def args[A: P] = P( ((id ~ "=" ~ !"=").? ~ expr).rep(sep = ",") ~ ",".? ).flatMapX{ x =>
     if (x.sliding(2).exists{case Seq(l, r) => l._1.isDefined && r._1.isEmpty case _ => false}) {
       Fail.opaque("no positional params after named params")
     } else {
@@ -400,7 +400,7 @@ class Parser(val currentFile: Path,
     }
   }
 
-  def params[_: P]: P[Expr.Params] = P( (id ~ ("=" ~ expr).?).rep(sep = ",") ~ ",".? ).flatMapX{ x =>
+  def params[A: P]: P[Expr.Params] = P( (id ~ ("=" ~ expr).?).rep(sep = ",") ~ ",".? ).flatMapX{ x =>
     val seen = collection.mutable.Set.empty[String]
     var overlap: String = null
     for((k, v) <- x){
@@ -416,7 +416,7 @@ class Parser(val currentFile: Path,
 
   }
 
-  def binaryop[_: P] = P(
+  def binaryop[A: P] = P(
     StringIn(
       "<<", ">>", "<=", ">=", "in", "==", "!=", "&&", "||",
       "*", "/", "%", "+", "-", "<", ">", "&", "^", "|"
@@ -424,7 +424,7 @@ class Parser(val currentFile: Path,
 
   ).!
 
-  def document[_: P]: P[(Expr, FileScope)] = P( expr ~  Pass(fileScope) ~ End )
+  def document[A: P]: P[(Expr, FileScope)] = P( expr ~  Pass(fileScope) ~ End )
 }
 
 final class Position(val fileScope: FileScope, val offset: Int) {
diff --git a/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala b/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala
index a2829fa2..fb89aa25 100644
--- a/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala
+++ b/sjsonnet/src/sjsonnet/PrettyYamlRenderer.scala
@@ -182,7 +182,7 @@ class PrettyYamlRenderer(out: Writer = new java.io.StringWriter(),
       out
     }
   }
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
     firstElementInArray = false
     var empty = true
     flushBuffer()
@@ -402,7 +402,7 @@ object PrettyYamlRenderer{
   def stringNeedsToBeQuoted(str: String) = {
     import fastparse._
     import NoWhitespace._
-    def yamlPunctuation[_: P] = P(
+    def yamlPunctuation[A: P] = P(
       // http://blogs.perl.org/users/tinita/2018/03/strings-in-yaml---to-quote-or-not-to-quote.html
       StringIn(
         "!", // ! Tag like !!null
@@ -420,7 +420,7 @@ object PrettyYamlRenderer{
         " " // leading or trailing empty spaces need quotes to define them
       )
     )
-    def yamlKeyword[_: P] = P(
+    def yamlKeyword[A: P] = P(
       StringIn(
         // https://makandracards.com/makandra/24809-yaml-keys-like-yes-or-no-evaluate-to-true-and-false
         // y|Y|yes|Yes|YES|n|N|no|No|NO
@@ -437,17 +437,17 @@ object PrettyYamlRenderer{
       )
     )
 
-    def digits[_: P] = P( CharsWhileIn("0-9") )
-    def yamlFloat[_: P] = P(
+    def digits[A: P] = P( CharsWhileIn("0-9") )
+    def yamlFloat[A: P] = P(
       (digits.? ~ "." ~ digits | digits ~ ".") ~ (("e" | "E") ~ ("+" | "-").? ~ digits).?
     )
-    def yamlOctalSuffix[_: P] = P( "x" ~ CharIn("1-9a-fA-F") ~ CharsWhileIn("0-9a-fA-F").? )
-    def yamlHexSuffix[_: P] = P( "o" ~ CharIn("1-7") ~ CharsWhileIn("0-7").? )
-    def yamlOctalHex[_: P] = P( "0" ~ (yamlOctalSuffix | yamlHexSuffix) )
-    def yamlNumber0[_: P] = P( ".inf" | yamlFloat | yamlOctalHex | digits )
+    def yamlOctalSuffix[A: P] = P( "x" ~ CharIn("1-9a-fA-F") ~ CharsWhileIn("0-9a-fA-F").? )
+    def yamlHexSuffix[A: P] = P( "o" ~ CharIn("1-7") ~ CharsWhileIn("0-7").? )
+    def yamlOctalHex[A: P] = P( "0" ~ (yamlOctalSuffix | yamlHexSuffix) )
+    def yamlNumber0[A: P] = P( ".inf" | yamlFloat | yamlOctalHex | digits )
 
     // Add a `CharIn` lookahead to bail out quickly if something cannot possibly be a number
-    def yamlNumber[_: P] = P( "-".? ~ yamlNumber0 )
+    def yamlNumber[A: P] = P( "-".? ~ yamlNumber0 )
 
     // Strings and numbers aren't the only scalars that YAML can understand.
     // ISO-formatted date and datetime literals are also parsed.
@@ -455,25 +455,25 @@ object PrettyYamlRenderer{
     // datetime:              2001-12-15T02:59:43.1Z
     // datetime_with_spaces:  2001-12-14 21:59:43.10 -5
 
-    def fourDigits[_: P] = P( CharIn("0-9") ~ CharIn("0-9") ~ CharIn("0-9") ~ CharIn("0-9") )
-    def oneTwoDigits[_: P] = P( CharIn("0-9") ~ CharIn("0-9").? )
-    def twoDigits[_: P] = P( CharIn("0-9") ~ CharIn("0-9") )
-    def dateTimeSuffix[_: P] = P(
+    def fourDigits[A: P] = P( CharIn("0-9") ~ CharIn("0-9") ~ CharIn("0-9") ~ CharIn("0-9") )
+    def oneTwoDigits[A: P] = P( CharIn("0-9") ~ CharIn("0-9").? )
+    def twoDigits[A: P] = P( CharIn("0-9") ~ CharIn("0-9") )
+    def dateTimeSuffix[A: P] = P(
       ("T" | " ") ~ twoDigits ~ ":" ~ twoDigits ~ ":" ~ twoDigits ~
         ("." ~ digits.?).? ~ ((" " | "Z").? ~ ("-".? ~ oneTwoDigits).?).?
     )
-    def yamlDate[_: P] = P( fourDigits ~ "-" ~ oneTwoDigits ~ "-" ~ oneTwoDigits ~ dateTimeSuffix.? )
+    def yamlDate[A: P] = P( fourDigits ~ "-" ~ oneTwoDigits ~ "-" ~ oneTwoDigits ~ dateTimeSuffix.? )
 
     // Not in the YAML, but included to match PyYAML behavior
-    def yamlTime[_: P] = P( twoDigits ~ ":" ~ twoDigits )
+    def yamlTime[A: P] = P( twoDigits ~ ":" ~ twoDigits )
 
-    def parser[_: P] = P(
+    def parser[A: P] = P(
       // Use a `&` lookahead to bail out early in the common case, so we don't
       // need to try parsing times/dates/numbers one by one
       yamlPunctuation | (&(CharIn(".0-9\\-")) ~ (yamlTime | yamlDate | yamlNumber) | yamlKeyword) ~ End
     )
 
-    fastparse.parse(str, parser(_)).isSuccess ||
+    fastparse.parse(str, parser).isSuccess ||
     str.contains(": ") || // Looks like a key-value pair
     str.contains(" #") || // Comments
     str.charAt(str.length - 1) == ':' || // Looks like a key-value pair
diff --git a/sjsonnet/src/sjsonnet/Renderer.scala b/sjsonnet/src/sjsonnet/Renderer.scala
index b7c61639..df9ab922 100644
--- a/sjsonnet/src/sjsonnet/Renderer.scala
+++ b/sjsonnet/src/sjsonnet/Renderer.scala
@@ -72,7 +72,7 @@ class Renderer(out: Writer = new java.io.StringWriter(),
     }
   }
 
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
     var empty = true
     flushBuffer()
     elemBuilder.append('{')
@@ -141,7 +141,7 @@ class PythonRenderer(out: Writer = new java.io.StringWriter(),
     out
   }
 
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
     flushBuffer()
     elemBuilder.append('{')
     depth += 1
@@ -203,7 +203,7 @@ case class MaterializeJsonRenderer(indent: Int = 4, escapeUnicode: Boolean = fal
     }
   }
 
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
     flushBuffer()
     elemBuilder.append('{')
     depth += 1
diff --git a/sjsonnet/src/sjsonnet/ScopedExprTransform.scala b/sjsonnet/src/sjsonnet/ScopedExprTransform.scala
index 54a6f730..a9d49796 100644
--- a/sjsonnet/src/sjsonnet/ScopedExprTransform.scala
+++ b/sjsonnet/src/sjsonnet/ScopedExprTransform.scala
@@ -40,12 +40,12 @@ class ScopedExprTransform extends ExprTransform {
           (transformBinds(preLocals), transformBinds(postLocals), transform(value))
         })
       })
-      if((f2 eq first) && (k2 eq key) && (v2 eq value) && (pre2 eq preLocals) && (post2 eq postLocals) && (r2, rest).zipped.forall(_ eq _)) e
+      if((f2 eq first) && (k2 eq key) && (v2 eq value) && (pre2 eq preLocals) && (post2 eq postLocals) && (r2 lazyZip rest).forall(_ eq _)) e
       else ObjComp(pos, pre2, k2, v2, plus, post2, f2.asInstanceOf[ForSpec], r2)
 
     case Comp(pos, value, first, rest) =>
       val (f2 :: r2, v2) = compSpecs(first :: rest.toList, () => transform(value))
-      if((f2 eq first) && (v2 eq value) && (r2, rest).zipped.forall(_ eq _)) e
+      if((f2 eq first) && (v2 eq value) && (r2 lazyZip rest).forall(_ eq _)) e
       else Comp(pos, v2, f2.asInstanceOf[ForSpec], r2.toArray)
 
     case e => rec(e)
@@ -62,13 +62,13 @@ class ScopedExprTransform extends ExprTransform {
     }
   }
 
-  protected[this] def transformFieldNameOnly(f: Member.Field): Member.Field = {
+  protected def transformFieldNameOnly(f: Member.Field): Member.Field = {
     val x = f.fieldName
     val x2 = transformFieldName(x)
     if(x2 eq x) f else f.copy(fieldName = x2)
   }
 
-  protected[this] def transformFieldNoName(f: Member.Field): Member.Field = {
+  protected def transformFieldNoName(f: Member.Field): Member.Field = {
     def g = {
       val y = f.args
       val z = f.rhs
@@ -80,9 +80,9 @@ class ScopedExprTransform extends ExprTransform {
     else nestedNames(f.args.names)(g)
   }
 
-  override protected[this] def transformField(f: Member.Field): Member.Field = ???
+  override protected def transformField(f: Member.Field): Member.Field = ???
 
-  protected[this] def compSpecs[T](a: List[CompSpec], value: () => T): (List[CompSpec], T) = a match {
+  protected def compSpecs[T](a: List[CompSpec], value: () => T): (List[CompSpec], T) = a match {
     case (c @ ForSpec(pos, name, cond)) :: cs =>
       val c2 = rec(c).asInstanceOf[ForSpec]
       nestedWith(c2.name, dynamicExpr) {
@@ -97,19 +97,19 @@ class ScopedExprTransform extends ExprTransform {
       (Nil, value())
   }
 
-  protected[this] def nestedNew[T](sc: Scope)(f: => T): T = {
+  protected def nestedNew[T](sc: Scope)(f: => T): T = {
     val oldScope = scope
     scope = sc
     try f finally { scope = oldScope }
   }
 
-  protected[this] def nestedWith[T](n: String, e: Expr)(f: => T): T =
+  protected def nestedWith[T](n: String, e: Expr)(f: => T): T =
     nestedNew(new Scope(scope.mappings.updated(n, new ScopedVal(e, scope, scope.size)), scope.size+1))(f)
 
-  protected[this] def nestedFileScope[T](fs: FileScope)(f: => T): T =
+  protected def nestedFileScope[T](fs: FileScope)(f: => T): T =
     nestedNew(emptyScope)(f)
 
-  protected[this] def nestedConsecutiveBindings[T](a: Array[Bind])(f: => Bind => Bind)(g: => T): (Array[Bind], T) = {
+  protected def nestedConsecutiveBindings[T](a: Array[Bind])(f: => Bind => Bind)(g: => T): (Array[Bind], T) = {
     if(a == null || a.length == 0) (a, g)
     else {
       val oldScope = scope
@@ -131,7 +131,7 @@ class ScopedExprTransform extends ExprTransform {
     }
   }
 
-  protected[this] def nestedBindings[T](a: Array[Bind])(f: => T): T = {
+  protected def nestedBindings[T](a: Array[Bind])(f: => T): T = {
     if(a == null || a.length == 0) f
     else {
       val newm = a.zipWithIndex.map { case (b, idx) =>
@@ -142,7 +142,7 @@ class ScopedExprTransform extends ExprTransform {
     }
   }
 
-  protected[this] def nestedObject[T](self0: Expr, super0: Expr)(f: => T): T = {
+  protected def nestedObject[T](self0: Expr, super0: Expr)(f: => T): T = {
     val self = new ScopedVal(self0, scope, scope.size)
     val sup = new ScopedVal(super0, scope, scope.size+1)
     val newm = {
@@ -152,10 +152,10 @@ class ScopedExprTransform extends ExprTransform {
     nestedNew(new Scope(newm, scope.size + 2))(f)
   }
 
-  protected[this] def nestedBindings[T](self0: Expr, super0: Expr, a: Array[Bind])(f: => T): T =
+  protected def nestedBindings[T](self0: Expr, super0: Expr, a: Array[Bind])(f: => T): T =
     nestedObject(self0, super0)(nestedBindings(a)(f))
 
-  protected[this] def nestedNames[T](a: Array[String])(f: => T): T = {
+  protected def nestedNames[T](a: Array[String])(f: => T): T = {
     if(a == null || a.length == 0) f
     else {
       val newm = a.zipWithIndex.map { case (n, idx) => (n, new ScopedVal(dynamicExpr, scope, scope.size + idx)) }
diff --git a/sjsonnet/src/sjsonnet/StaticOptimizer.scala b/sjsonnet/src/sjsonnet/StaticOptimizer.scala
index 6e1e2854..3b2b4bd0 100644
--- a/sjsonnet/src/sjsonnet/StaticOptimizer.scala
+++ b/sjsonnet/src/sjsonnet/StaticOptimizer.scala
@@ -42,7 +42,7 @@ class StaticOptimizer(
 
     case e @ Id(pos, name) =>
       scope.get(name) match {
-        case ScopedVal(v: Val with Expr, _, _) => v
+        case ScopedVal(v: (Val & Expr), _, _) => v
         case ScopedVal(_, _, idx) => ValidId(pos, name, idx)
         case null if name == "std" => std
         case _ => failOrWarn("Unknown variable: "+name, e)
@@ -98,7 +98,7 @@ class StaticOptimizer(
     case _ => false
   }
 
-  override protected[this] def transformFieldName(f: FieldName): FieldName = f match {
+  override protected def transformFieldName(f: FieldName): FieldName = f match {
     case FieldName.Dyn(x) =>
       transform(x) match {
         case x2: Val.Str =>
@@ -201,7 +201,7 @@ class StaticOptimizer(
     while(i < target.length) {
       if(target(i) == null) {
         params.defaultExprs(i) match {
-          case v: Val with Expr => target(i) = v
+          case v: (Val & Expr) => target(i) = v
           case _ => return null // no default or non-constant
         }
       }
diff --git a/sjsonnet/src/sjsonnet/Std.scala b/sjsonnet/src/sjsonnet/Std.scala
index 9c79d0cc..19a97a0f 100644
--- a/sjsonnet/src/sjsonnet/Std.scala
+++ b/sjsonnet/src/sjsonnet/Std.scala
@@ -697,7 +697,7 @@ class Std {
     }
 
     private class Spec1Str(_a: Val.Arr) extends Val.Builtin1("b") {
-      private[this] val a =
+      private val a =
         ArrayOps.sortInPlaceBy(ArrayOps.distinctBy(_a.asLazyArray)(_.asInstanceOf[Val.Str].value))(_.asInstanceOf[Val.Str].value)
         // 2.13+: _a.asLazyArray.distinctBy(_.asInstanceOf[Val.Str].value).sortInPlaceBy(_.asInstanceOf[Val.Str].value)
 
@@ -750,8 +750,8 @@ class Std {
         case (l: Val.Obj, r: Val.Obj) =>
           val kvs = for {
             k <- (l.visibleKeyNames ++ r.visibleKeyNames).distinct
-            val lValue = Option(l.valueRaw(k, l, pos)(ev))
-            val rValue = Option(r.valueRaw(k, r, pos)(ev))
+            lValue = Option(l.valueRaw(k, l, pos)(ev))
+            rValue = Option(r.valueRaw(k, r, pos)(ev))
             if !rValue.exists(_.isInstanceOf[Val.Null])
           } yield (lValue, rValue) match{
             case (Some(lChild), None) => k -> createMember{lChild}
@@ -759,7 +759,7 @@ class Std {
             case (_, Some(rChild)) => k -> createMember{recSingle(rChild)}
           }
 
-          Val.Obj.mk(mergePosition, kvs:_*)
+          Val.Obj.mk(mergePosition, kvs*)
 
         case (_, _) => recSingle(r)
       }
@@ -767,11 +767,11 @@ class Std {
         case obj: Val.Obj =>
           val kvs = for{
             k <- obj.visibleKeyNames
-            val value = obj.value(k, pos, obj)(ev)
+            value = obj.value(k, pos, obj)(ev)
             if !value.isInstanceOf[Val.Null]
           } yield (k, createMember{recSingle(value)})
 
-          Val.Obj.mk(obj.pos, kvs:_*)
+          Val.Obj.mk(obj.pos, kvs*)
 
         case _ => v
       }
@@ -1228,7 +1228,7 @@ class Std {
             v = rec(o.value(k, pos.fileScope.noOffsetPos)(ev))
             if filter(v)
           }yield (k, new Val.Obj.ConstMember(false, Visibility.Normal, v))
-          Val.Obj.mk(pos, bindings: _*)
+          Val.Obj.mk(pos, bindings*)
         case a: Val.Arr =>
           new Val.Arr(pos, a.asStrictArray.map(rec).filter(filter).map(identity))
         case _ => x
@@ -1272,7 +1272,7 @@ class Std {
             Val.Str(self.pos, fs.currentFile.relativeToString(ev.wd))
         }
       )
-    ): _*
+    )*
   )
 
   def builtin[R: ReadWriter, T1: ReadWriter](name: String, p1: String)
@@ -1429,7 +1429,7 @@ class Std {
   def getAllKeys(ev: EvalScope, v1: Val.Obj): Array[String] =
     maybeSortKeys(ev, v1.allKeyNames)
   
-  @inline private[this] def maybeSortKeys(ev: EvalScope, keys: Array[String]): Array[String] =
+  @inline private def maybeSortKeys(ev: EvalScope, keys: Array[String]): Array[String] =
     if(ev.settings.preserveOrder) keys else keys.sorted
 
   def getObjValuesFromKeys(pos: Position, ev: EvalScope, v1: Val.Obj, keys: Array[String]): Val.Arr =
diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala
index db1ac04a..7359aa8a 100644
--- a/sjsonnet/src/sjsonnet/Val.scala
+++ b/sjsonnet/src/sjsonnet/Val.scala
@@ -17,7 +17,7 @@ import scala.reflect.ClassTag
  * are all wrapped in [[Lazy]] and only truly evaluated on-demand
  */
 abstract class Lazy {
-  protected[this] var cached: Val = null
+  protected var cached: Val = null
   def compute(): Val
   final def force: Val = {
     if(cached == null)  cached = compute()
@@ -42,7 +42,7 @@ sealed abstract class Val extends Lazy {
     if (implicitly[ClassTag[T]].runtimeClass.isInstance(this)) this.asInstanceOf[T]
     else Error.fail("Expected " + implicitly[PrettyNamed[T]].s + ", found " + prettyName)
 
-  private[this] def failAs(err: String): Nothing =
+  private def failAs(err: String): Nothing =
     Error.fail("Wrong parameter type: expected " + err + ", got " + prettyName)
 
   def asString: String = failAs("String")
@@ -90,13 +90,13 @@ object Val{
     override def asDouble: Double = value
   }
 
-  class Arr(val pos: Position, private val value: Array[_ <: Lazy]) extends Literal {
+  class Arr(val pos: Position, private val value: Array[? <: Lazy]) extends Literal {
     def prettyName = "array"
     override def asArr: Arr = this
     def length: Int = value.length
     def force(i: Int) = value(i).force
 
-    def asLazy(i: Int) = value(i)
+    def asLazy(i: Int): Lazy = value(i)
     def asLazyArray: Array[Lazy] = value.asInstanceOf[Array[Lazy]]
     def asStrictArray: Array[Val] = value.map(_.force)
 
@@ -140,17 +140,17 @@ object Val{
   }
 
   final class Obj(val pos: Position,
-                  private[this] var value0: util.LinkedHashMap[String, Obj.Member],
+                  private var value0: util.LinkedHashMap[String, Obj.Member],
                   static: Boolean,
                   triggerAsserts: Val.Obj => Unit,
                   `super`: Obj,
                   valueCache: mutable.HashMap[Any, Val] = mutable.HashMap.empty[Any, Val],
-                  private[this] var allKeys: util.LinkedHashMap[String, java.lang.Boolean] = null) extends Literal with Expr.ObjBody {
+                  private var allKeys: util.LinkedHashMap[String, java.lang.Boolean] = null) extends Literal with Expr.ObjBody {
     var asserting: Boolean = false
 
     def getSuper = `super`
 
-    private[this] def getValue0: util.LinkedHashMap[String, Obj.Member] = {
+    private def getValue0: util.LinkedHashMap[String, Obj.Member] = {
       if(value0 == null) {
         val value0 = new java.util.LinkedHashMap[String, Val.Obj.Member]
         allKeys.forEach { (k, _) =>
@@ -353,7 +353,7 @@ object Val{
 
     override def asFunc: Func = this
 
-    def apply(argsL: Array[_ <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val = {
+    def apply(argsL: Array[? <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val = {
       val simple = namedNames == null && params.names.length == argsL.length
       val funDefFileScope: FileScope = pos match { case null => outerPos.fileScope case p => p.fileScope }
       //println(s"apply: argsL: ${argsL.length}, namedNames: $namedNames, paramNames: ${params.names.mkString(",")}")
@@ -491,7 +491,7 @@ object Val{
 
     def evalRhs(arg1: Val, ev: EvalScope, pos: Position): Val
 
-    override def apply(argVals: Array[_ <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
+    override def apply(argVals: Array[? <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
       if(namedNames == null && argVals.length == 1) evalRhs(argVals(0).force, ev, outerPos)
       else super.apply(argVals, namedNames, outerPos)
 
@@ -506,7 +506,7 @@ object Val{
 
     def evalRhs(arg1: Val, arg2: Val, ev: EvalScope, pos: Position): Val
 
-    override def apply(argVals: Array[_ <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
+    override def apply(argVals: Array[? <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
       if(namedNames == null && argVals.length == 2)
         evalRhs(argVals(0).force, argVals(1).force, ev, outerPos)
       else super.apply(argVals, namedNames, outerPos)
@@ -522,7 +522,7 @@ object Val{
 
     def evalRhs(arg1: Val, arg2: Val, arg3: Val, ev: EvalScope, pos: Position): Val
 
-    override def apply(argVals: Array[_ <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
+    override def apply(argVals: Array[? <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
       if(namedNames == null && argVals.length == 3)
         evalRhs(argVals(0).force, argVals(1).force, argVals(2).force, ev, outerPos)
       else super.apply(argVals, namedNames, outerPos)
@@ -534,7 +534,7 @@ object Val{
 
     def evalRhs(arg1: Val, arg2: Val, arg3: Val, arg4: Val, ev: EvalScope, pos: Position): Val
 
-    override def apply(argVals: Array[_ <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
+    override def apply(argVals: Array[? <: Lazy], namedNames: Array[String], outerPos: Position)(implicit ev: EvalScope): Val =
       if(namedNames == null && argVals.length == 4)
         evalRhs(argVals(0).force, argVals(1).force, argVals(2).force, argVals(3).force, ev, outerPos)
       else super.apply(argVals, namedNames, outerPos)
diff --git a/sjsonnet/src/sjsonnet/ValScope.scala b/sjsonnet/src/sjsonnet/ValScope.scala
index d930cbaf..fbb3e602 100644
--- a/sjsonnet/src/sjsonnet/ValScope.scala
+++ b/sjsonnet/src/sjsonnet/ValScope.scala
@@ -35,12 +35,11 @@ final class ValScope private (val bindings: Array[Lazy]) extends AnyVal {
         i += 1
         j += 1
       }
-      b
     }
     new ValScope(b)
   }
 
-  def extendSimple(newBindingsV: Array[_ <: Lazy]) = {
+  def extendSimple(newBindingsV: Array[? <: Lazy]) = {
     if(newBindingsV == null || newBindingsV.length == 0) this
     else {
       val b = Arrays.copyOf(bindings, bindings.length + newBindingsV.length)
@@ -76,6 +75,6 @@ final class ValScope private (val bindings: Array[Lazy]) extends AnyVal {
 }
 
 object ValScope{
-  private[this] val emptyArr = new Array[Lazy](0)
+  private val emptyArr = new Array[Lazy](0)
   def empty = new ValScope(emptyArr)
 }
diff --git a/sjsonnet/src/sjsonnet/ValVisitor.scala b/sjsonnet/src/sjsonnet/ValVisitor.scala
index 441acabe..e6af5a66 100644
--- a/sjsonnet/src/sjsonnet/ValVisitor.scala
+++ b/sjsonnet/src/sjsonnet/ValVisitor.scala
@@ -11,16 +11,16 @@ import scala.collection.mutable
 class ValVisitor(pos: Position) extends JsVisitor[Val, Val] { self =>
   def visitArray(length: Int, index: Int): ArrVisitor[Val, Val] = new ArrVisitor[Val, Val] {
     val a = new mutable.ArrayBuilder.ofRef[Lazy]
-    def subVisitor: Visitor[_, _] = self
+    def subVisitor: Visitor[?, ?] = self
     def visitValue(v: Val, index: Int): Unit = a.+=(v)
     def visitEnd(index: Int): Val = new Val.Arr(pos, a.result())
   }
 
-  def visitObject(length: Int, index: Int): ObjVisitor[Val, Val] = new ObjVisitor[Val, Val] {
+  def visitJsonableObject(length: Int, index: Int): ObjVisitor[Val, Val] = new ObjVisitor[Val, Val] {
     val cache = mutable.HashMap.empty[Any, Val]
     val allKeys = new util.LinkedHashMap[String, java.lang.Boolean]
     var key: String = null
-    def subVisitor: Visitor[_, _] = self
+    def subVisitor: Visitor[?, ?] = self
     def visitKey(index: Int) = upickle.core.StringVisitor
     def visitKeyValue(s: Any): Unit = key = s.toString
     def visitValue(v: Val, index: Int): Unit = {
@@ -39,7 +39,7 @@ class ValVisitor(pos: Position) extends JsVisitor[Val, Val] { self =>
   def visitFloat64StringParts(s: CharSequence, decIndex: Int, expIndex: Int, index: Int): Val =
     Val.Num(pos,
       if (decIndex != -1 || expIndex != -1) s.toString.toDouble
-      else upickle.core.Util.parseIntegralNum(s, decIndex, expIndex, index)
+      else upickle.core.ParseUtils.parseIntegralNum(s, decIndex, expIndex, index).toDouble
     )
 
   def visitString(s: CharSequence, index: Int): Val = Val.Str(pos, s.toString)
diff --git a/sjsonnet/src/sjsonnet/YamlRenderer.scala b/sjsonnet/src/sjsonnet/YamlRenderer.scala
index b542fad2..1f6945e8 100644
--- a/sjsonnet/src/sjsonnet/YamlRenderer.scala
+++ b/sjsonnet/src/sjsonnet/YamlRenderer.scala
@@ -20,7 +20,7 @@ class YamlRenderer(_out: StringWriter = new java.io.StringWriter(), indentArrayI
     elemBuilder.writeOutToIfLongerThan(_out, if (depth <= 0 || topLevel) 0 else 1000)
   }
 
-  private[this] def appendString(s: String) = {
+  private def appendString(s: String) = {
     val len = s.length
     var i = 0
     elemBuilder.ensureLength(len)
@@ -48,7 +48,7 @@ class YamlRenderer(_out: StringWriter = new java.io.StringWriter(), indentArrayI
       }
       depth -= 1
     } else {
-      upickle.core.RenderUtils.escapeChar(unicodeCharBuilder, elemBuilder, s, true)
+      upickle.core.RenderUtils.escapeChar(unicodeCharBuilder, elemBuilder, s, escapeUnicode = true, wrapQuotes = true)
     }
     flushCharBuilder()
     _out
@@ -116,7 +116,7 @@ class YamlRenderer(_out: StringWriter = new java.io.StringWriter(), indentArrayI
       _out
     }
   }
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
     var empty = true
     flushBuffer()
     if (!topLevel) depth += 1
diff --git a/sjsonnet/test/src-jvm/sjsonnet/BufferedRandomAccessFileTests.scala b/sjsonnet/test/src-jvm/sjsonnet/BufferedRandomAccessFileTests.scala
index 0ad1d221..ba4be747 100644
--- a/sjsonnet/test/src-jvm/sjsonnet/BufferedRandomAccessFileTests.scala
+++ b/sjsonnet/test/src-jvm/sjsonnet/BufferedRandomAccessFileTests.scala
@@ -166,8 +166,8 @@ object BufferedRandomAccessFileTests extends TestSuite {
       }
     }
 
-  // Test content
-  val testContent = "Hello, World! This is a test file with various content to thoroughly test the BufferedRandomAccessFile."
+    // Test content
+    val testContent = "Hello, World! This is a test file with various content to thoroughly test the BufferedRandomAccessFile."
 
     test("bufferReloadsAndEdgeReads") {
       val bufferedFile = new BufferedRandomAccessFile(tempFile.getAbsolutePath, 15)
diff --git a/sjsonnet/test/src/sjsonnet/DummyPath.scala b/sjsonnet/test/src/sjsonnet/DummyPath.scala
index 7d65728b..2b76dac1 100644
--- a/sjsonnet/test/src/sjsonnet/DummyPath.scala
+++ b/sjsonnet/test/src/sjsonnet/DummyPath.scala
@@ -9,13 +9,13 @@ case class DummyPath(segments: String*) extends Path{
 
   def debugRead(): Option[String] = None
 
-  def parent(): Path = DummyPath(segments.dropRight(1):_*)
+  def parent(): Path = DummyPath(segments.dropRight(1)*)
 
   def segmentCount(): Int = segments.length
 
   def last: String = segments.last
 
-  def /(s: String): Path = DummyPath(segments :+ s:_*)
+  def /(s: String): Path = DummyPath(segments :+ s*)
 
   def renderOffsetStr(offset: Int, loadedFileContents: mutable.HashMap[Path, Array[Int]]): String = {
     segments.mkString("/") + ":" + offset
diff --git a/sjsonnet/test/src/sjsonnet/OldRenderer.scala b/sjsonnet/test/src/sjsonnet/OldRenderer.scala
index 33de0d9d..f9cf5045 100644
--- a/sjsonnet/test/src/sjsonnet/OldRenderer.scala
+++ b/sjsonnet/test/src/sjsonnet/OldRenderer.scala
@@ -63,7 +63,7 @@ class OldRenderer(out: Writer = new java.io.StringWriter(),
     }
   }
 
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[Writer, Writer] {
     var empty = true
     flushBuffer()
     out.append('{')
diff --git a/sjsonnet/test/src/sjsonnet/OldYamlRenderer.scala b/sjsonnet/test/src/sjsonnet/OldYamlRenderer.scala
index 533f4ee4..b335b505 100644
--- a/sjsonnet/test/src/sjsonnet/OldYamlRenderer.scala
+++ b/sjsonnet/test/src/sjsonnet/OldYamlRenderer.scala
@@ -84,7 +84,7 @@ class OldYamlRenderer(out: StringWriter = new java.io.StringWriter(), indentArra
       out
     }
   }
-  override def visitObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
+  override def visitJsonableObject(length: Int, index: Int) = new ObjVisitor[StringWriter, StringWriter] {
     var empty = true
     flushBuffer()
     if (!topLevel) depth += 1
diff --git a/sjsonnet/test/src/sjsonnet/ParserTests.scala b/sjsonnet/test/src/sjsonnet/ParserTests.scala
index 37da8c5b..0594d8a9 100644
--- a/sjsonnet/test/src/sjsonnet/ParserTests.scala
+++ b/sjsonnet/test/src/sjsonnet/ParserTests.scala
@@ -6,8 +6,8 @@ import Expr._
 import fastparse.Parsed
 import Val.{True, Num}
 object ParserTests extends TestSuite{
-  def parse(s: String, strictImportSyntax: Boolean = false) = fastparse.parse(s, new Parser(null, strictImportSyntax, mutable.HashMap.empty, mutable.HashMap.empty).document(_)).get.value._1
-  def parseErr(s: String, strictImportSyntax: Boolean = false) = fastparse.parse(s, new Parser(null, strictImportSyntax, mutable.HashMap.empty, mutable.HashMap.empty).document(_), verboseFailures = true).asInstanceOf[Parsed.Failure].msg
+  def parse(s: String, strictImportSyntax: Boolean = false) = fastparse.parse(s, new Parser(null, strictImportSyntax, mutable.HashMap.empty, mutable.HashMap.empty).document(using _)).get.value._1
+  def parseErr(s: String, strictImportSyntax: Boolean = false) = fastparse.parse(s, new Parser(null, strictImportSyntax, mutable.HashMap.empty, mutable.HashMap.empty).document(using _), verboseFailures = true).asInstanceOf[Parsed.Failure].msg
   val dummyFS = new FileScope(null)
   def pos(i: Int) = new Position(dummyFS, i)
   def tests = Tests{