All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.datasonnet.jsonnet.Val.scala Maven / Gradle / Ivy

package com.datasonnet.jsonnet

/*-
 * Copyright 2019-2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import com.datasonnet.jsonnet.Expr.Member.Visibility
import com.datasonnet.jsonnet.Expr.Params
import ujson.Value

import scala.annotation.tailrec
import scala.collection.mutable
import scala.reflect.ClassTag

/**
  * [[Val]]s represented Jsonnet values that are the result of evaluating
  * a Jsonnet program. The [[Val]] data structure is essentially a JSON tree,
  * except evaluation of object attributes and array contents are lazy, and
  * the tree can contain functions.
  */
sealed trait Val{
  def prettyName: String
  def cast[T: ClassTag: PrettyNamed] =
    if (implicitly[ClassTag[T]].runtimeClass.isInstance(this)) this.asInstanceOf[T]
    else throw new Error.Delegate(
      "Expected " + implicitly[PrettyNamed[T]].s + ", found " + prettyName
    )
}
class PrettyNamed[T](val s: String)
object PrettyNamed{
  implicit def strName: PrettyNamed[Val.Str] = new PrettyNamed("string")
  implicit def numName: PrettyNamed[Val.Num] = new PrettyNamed("number")
  implicit def arrName: PrettyNamed[Val.Arr] = new PrettyNamed("array")
  implicit def objName: PrettyNamed[Val.Obj] = new PrettyNamed("object")
  implicit def funName: PrettyNamed[Val.Func] = new PrettyNamed("function")
}
object Val{

  /**
    * [[Lazy]] models lazy evaluation within a Jsonnet program. Lazily
    * evaluated dictionary values, array contents, or function parameters
    * are all wrapped in [[Lazy]] and only truly evaluated on-demand
    */
  class Lazy(calc0: => Val){
    lazy val force = calc0
  }
  object Lazy{
    def apply(calc0: => Val) = new Lazy(calc0)
  }

  def bool(b: Boolean) = if (b) True else False
  sealed trait Bool extends Val{
  }
  case object True extends Bool{
    def prettyName = "boolean"
  }
  case object False extends Bool{
    def prettyName = "boolean"
  }
  case object Null extends Val{
    def prettyName = "null"
  }
  case class Str(value: String) extends Val{
    def prettyName = "string"
  }
  case class Num(value: Double) extends Val{
    def prettyName = "number"
  }
  case class Arr(value: Seq[Lazy]) extends Val{
    def prettyName = "array"
  }
  object Obj{

    case class Member(add: Boolean,
                      visibility: Visibility,
                      invoke: (Obj, Option[Obj], FileScope, EvalScope) => Val,
                      cached: Boolean = true)


  }
  final class Obj(value0: mutable.Map[String, Obj.Member],
                  triggerAsserts: Val.Obj => Unit,
                  `super`: Option[Obj]) extends Val{

    def getSuper = `super`

    @tailrec def triggerAllAsserts(obj: Val.Obj): Unit = {
      triggerAsserts(obj)
      `super` match {
        case Some(s) => s.triggerAllAsserts(obj)
        case None => ()
      }
    }

    def addSuper(lhs: Val.Obj): Val.Obj = {
      `super` match{
        case None => new Val.Obj(value0, _ => (), Some(lhs))
        case Some(x) => new Val.Obj(value0, _ => (), Some(x.addSuper(lhs)))
      }
    }

    def prettyName = "object"

    def foreachVisibleKey(output: (String, Visibility) => Unit): Unit = {
      for(s <- this.`super`) s.foreachVisibleKey(output)
      for(t <- value0) output(t._1, t._2.visibility)
    }

    def getVisibleKeys() = {
      val mapping = mutable.LinkedHashMap.empty[String, Boolean]
      foreachVisibleKey{ (k, sep) =>
        (mapping.get(k), sep) match{
          case (None, Visibility.Hidden) => mapping(k) = true
          case (None, _)    => mapping(k) = false

          case (Some(false), Visibility.Hidden) => mapping(k) = true
          case (Some(true), Visibility.Unhide) => mapping(k) = false
          case (Some(x), _) => mapping(k) = x
        }
      }
      mapping
    }
    private[this] val valueCache = collection.mutable.Map.empty[Any, Val]

    def value(k: String,
              offset: Int)
             (implicit fileScope: FileScope, evaluator: EvalScope): Val = {
      value(k, offset, this, null)
    }

    def value(k: String,
              offset: Int,
              self: Obj = this,
              defaultValue: Value)
             (implicit fileScope: FileScope, evaluator: EvalScope): Val = {

      val cacheKey = if(self eq this) k else (k, self)

      valueCache.get(cacheKey) match{
        case Some(res) => res
        case None =>
          valueRaw(k, self, offset) match{
            case Some((x, cached)) =>
              if (cached) valueCache(cacheKey) = x
              x
            case None =>
              if (defaultValue == null) Error.fail("Field does not exist: " + k, offset)
              else Materializer.reverse(defaultValue)
          }
      }
    }

    def mergeMember(l: Val,
                    r: Val,
                    offset: Int)
                   (implicit fileScope: FileScope, evaluator: EvalScope) = (l, r) match{
      case (Val.Str(l), Val.Str(r)) => Val.Str(l + r)
      case (Val.Num(l), Val.Num(r)) => Val.Num(l + r)
      case (Val.Arr(l), Val.Arr(r)) => Val.Arr(l ++ r)
      case (l: Val.Obj, r: Val.Obj) => r.addSuper(l)
      case (Val.Str(l), r) =>
        try Val.Str(l + evaluator.materialize(r).transform(new Renderer()).toString)
        catch Error.tryCatchWrap(offset)
      case (l, Val.Str(r)) =>
        try Val.Str(evaluator.materialize(l).transform(new Renderer()).toString + r)
        catch Error.tryCatchWrap(offset)
    }

    @tailrec def valueCached(k: String): Option[Boolean] = this.value0.get(k) match{
      case Some(m) => Some(m.cached)

      case None => this.`super` match{
        case None => None
        case Some(s) => s.valueCached(k)
      }
    }

    def valueRaw(k: String,
                 self: Obj,
                 offset: Int)
                (implicit fileScope: FileScope, evaluator: EvalScope): Option[(Val, Boolean)] = {
      this.value0.get(k) match{
        case Some(m) =>
          this.`super` match{
            case Some(s) if m.add =>
              val merged = s.valueRaw(k, self, offset) match{
                case None => m.invoke(self, this.`super`, fileScope, evaluator)
                case Some((supValue, supCached)) =>
                  mergeMember(supValue, m.invoke(self, this.`super`, fileScope, evaluator), offset)
              }

              Some(merged -> m.cached)

            case _ =>
              Some(m.invoke(self, this.`super`, fileScope, evaluator) -> m.cached)
          }

        case None => this.`super` match{
          case None => None
          case Some(s) => s.valueRaw(k, self, offset)
        }
      }
    }

    @tailrec def containsKey(k: String): Boolean = {
      this.value0.contains(k) || {
        this.`super` match {
          case None => false
          case Some(s) => s.containsKey(k)
        }
      }
    }
  }

  case class Func(defSiteScopes: Option[(ValScope, FileScope)],
                  params: Params,
                  evalRhs: (ValScope, String, EvalScope, FileScope, Int) => Val,
                  evalDefault: (Expr, ValScope, EvalScope) => Val = null) extends Val{
    def prettyName = "function"
    def apply(args: Seq[(Option[String], Lazy)],
              thisFile: String,
              outerOffset: Int)
             (implicit fileScope: FileScope, evaluator: EvalScope) = {

      lazy val defaultArgsBindings = params
        .defaults
        .map{ case (index, default) => (index, Lazy(evalDefault(default, newScope, evaluator)))}

      lazy val passedArgsBindings = try
        args.zipWithIndex.map{
          case ((Some(name), v), _) =>
            val argIndex = params.argIndices.getOrElse(
              name,
              Error.fail(s"Function has no parameter $name", outerOffset)
            )
            (argIndex, v)
          case ((None, v), i) => (params.args(i)._3, v)
        }
      catch{ case e: IndexOutOfBoundsException =>
        Error.fail(
          "Too many args, function has " + params.args.length + " parameter(s)",
          outerOffset
        )
      }

      lazy val newScope: ValScope = {
        var max = -1
        val builder = Array.newBuilder[(Int, (Option[Val.Obj], Option[Val.Obj]) => Lazy)]
        for(t <- defaultArgsBindings){
          val (i, v) = t
          if (i > max) max = i
          builder += (i, (self: Option[Val.Obj], sup: Option[Val.Obj]) => v)
        }

        for(t <- passedArgsBindings){
          val (i, v) = t
          if (i > max) max = i
          builder += (i, (self: Option[Val.Obj], sup: Option[Val.Obj]) => v)
        }

        defSiteScopes match{
          case None => new ValScope(
            None,
            None,
            None,
            {
              val arr = new Array[Lazy](max + 1)
              for((i, v) <- builder.result()) arr(i) = v(null, None)
              arr
            }
          )
          case Some((s, fs)) => s.extend(builder.result())
        }
      }

      val funDefFileScope: FileScope = defSiteScopes match {case None => fileScope case Some((s, fs)) => fs}
      validateFunctionCall(passedArgsBindings, params, outerOffset, funDefFileScope)

      evalRhs(
        newScope,
        thisFile,
        evaluator,
        funDefFileScope,
        outerOffset
      )
    }

    def validateFunctionCall(passedArgsBindings: Seq[(Int, Lazy)],
                             params: Params,
                             outerOffset: Int,
                             defSiteFileScope: FileScope)
                            (implicit fileScope: FileScope, eval: EvalScope): Unit = {

      val seen = mutable.BitSet.empty
      val repeats = mutable.BitSet.empty
      for(t <- passedArgsBindings){
        if (!seen(t._1)) seen.add(t._1)
        else repeats.add(t._1)
      }

      Error.failIfNonEmpty(
        repeats,
        outerOffset,
        (plural, names) => s"Function parameter$plural $names passed more than once"
      )

      Error.failIfNonEmpty(
        params.noDefaultIndices.filter(!seen(_)),
        outerOffset,
        (plural, names) => s"Function parameter$plural $names not bound in call"
      )(defSiteFileScope, eval) // pass the definition site for the correct error message/names to be resolved

      Error.failIfNonEmpty(
        seen.filter(!params.allIndices(_)),
        outerOffset,
        (plural, names) => s"Function has no parameter$plural $names"
      )
    }
  }
}

/**
  * [[EvalScope]] models the per-evaluator context that is propagated
  * throughout the Jsonnet evaluation.
  */
trait EvalScope extends EvalErrorScope{
  def visitExpr(expr: Expr)
               (implicit scope: ValScope, fileScope: FileScope): Val

  def materialize(v: Val): ujson.Value

  val emptyMaterializeFileScope = new FileScope(wd / "(materialize)", Map())

  val preserveOrder: Boolean = false

  val defaultValue: ujson.Value = null
}
object ValScope{
  def empty(size: Int) = new ValScope(None, None, None, new Array(size))
}

/**
  * [[ValScope]]s which model the lexical scopes within
  * a Jsonnet file that bind variable names to [[Val]]s, as well as other
  * contextual information like `self` `this` or `super`.
  *
  * Note that scopes are standalone, and nested scopes are done by copying
  * and updating the array of bindings rather than using a linked list. This
  * is because the bindings array is typically pretty small and the constant
  * factor overhead from a cleverer data structure dominates any algorithmic
  * improvements
  *
  * The bindings array is private and only copy-on-write, so for nested scopes
  * which do not change it (e.g. those just updating `dollar0` or `self0`) the
  * bindings array can be shared cheaply.
  */
class ValScope(val dollar0: Option[Val.Obj],
               val self0: Option[Val.Obj],
               val super0: Option[Val.Obj],
               bindings0: Array[Val.Lazy]) {

  def bindings(k: Int): Option[Val.Lazy] = bindings0(k) match{
    case null => None
    case v => Some(v)
  }

  def extend(newBindings: TraversableOnce[(Int, (Option[Val.Obj], Option[Val.Obj]) => Val.Lazy)] = Nil,
             newDollar: Option[Val.Obj] = null,
             newSelf: Option[Val.Obj] = null,
             newSuper: Option[Val.Obj] = null) = {
    val dollar = if (newDollar != null) newDollar else dollar0
    val self = if (newSelf != null) newSelf else self0
    val sup = if (newSuper != null) newSuper else super0
    new ValScope(
      dollar,
      self,
      sup,
      if (newBindings.isEmpty) bindings0
      else{
        val newArr = java.util.Arrays.copyOf(bindings0, bindings0.length)
        for((i, v) <- newBindings) newArr(i) = v.apply(self, sup)
        newArr
      }
    )
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy