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

com.datasonnet.jsonnet.Evaluator.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 Expr.{Error => _, _}
import fastparse.Parsed
import com.datasonnet.jsonnet.Expr.Member.Visibility
import ujson.Value

import scala.collection.mutable
import scala.util.{Failure, Success, Try}

/**
  * Recursively walks the [[Expr]] trees to convert them into into [[Val]]
  * objects that can be materialized to JSON.
  *
  * Performs import resolution and parsing on-demand when the relevant nodes
  * in the syntax tree are reached, and caches the evaluated result of each
  * imported module to be re-used. Parsing is cached separatedly by an external
  * `parseCache`.
  */
class Evaluator(parseCache: collection.mutable.Map[String, fastparse.Parsed[(Expr, Map[String, Int])]],
                val extVars: Map[String, ujson.Value],
                val wd: Path,
                importer: (Path, String) => Option[(Path, String)],
                override val preserveOrder: Boolean = false,
                override val defaultValue: Value = null) extends EvalScope{
  implicit def evalScope: EvalScope = this

  val loadedFileContents = mutable.Map.empty[Path, String]
  def loadCachedSource(p: Path) = loadedFileContents.get(p)
  def materialize(v: Val): Value = Materializer.apply(v)
  val cachedImports = collection.mutable.Map.empty[Path, Val]

  val cachedImportedStrings = collection.mutable.Map.empty[Path, String]
  override def visitExpr(expr: Expr)(implicit scope: ValScope, fileScope: FileScope): Val = visitExpr(expr, false)
  def visitExpr(expr: Expr, tryCatch: Boolean = false)
               (implicit scope: ValScope, fileScope: FileScope): Val = try expr match{
    case Null(offset) => Val.Null
    case Parened(offset, inner) => visitExpr(inner)
    case True(offset) => Val.True
    case False(offset) => Val.False
    case Self(offset) => scope.self0.getOrElse(Error.fail("Cannot use `self` outside an object", offset))

    case BinaryOp(offset, lhs, Expr.BinaryOp.`in`, Super(_)) =>
      scope.super0 match{
        case None => Val.False
        case Some(sup) =>
          val key = visitExpr(lhs).cast[Val.Str]
          Val.bool(sup.containsKey(key.value))
      }

    case $(offset) => scope.dollar0.getOrElse(Error.fail("Cannot use `$` outside an object", offset))
    case Str(offset, value) => Val.Str(value)
    case Num(offset, value) => Val.Num(value)
    case Id(offset, value) => visitId(offset, value)

    case Arr(offset, value) => Val.Arr(value.map(v => Val.Lazy(visitExpr(v))))
    case Obj(offset, value) => visitObjBody(value)

    case UnaryOp(offset, op, value) => visitUnaryOp(op, value)

    case BinaryOp(offset, lhs, op, rhs) => {
      visitBinaryOp(offset, lhs, op, rhs)
    }
    case AssertExpr(offset, Member.AssertStmt(value, msg), returned) =>
      visitAssert(offset, value, msg, returned)

    case LocalExpr(offset, bindings, returned) =>
      lazy val newScope: ValScope = scope.extend(visitBindings(bindings.iterator, (self, sup) => newScope))
      visitExpr(returned)(newScope, implicitly)

    case Import(offset, value) => visitImport(offset, value)
    case ImportStr(offset, value) => visitImportStr(offset, value)
    case Expr.Error(offset, value) => visitError(offset, value)
    case Apply(offset, value, Args(args)) => visitApply(offset, value, args)

    case Select(offset, value, name) => visitSelect(offset, value, name, tryCatch)

    case Lookup(offset, value, index) => visitLookup(offset, value, index)

    case Slice(offset, value, start, end, stride) => visitSlice(offset, value, start, end, stride)
    case Function(offset, params, body) => visitMethod(body, params, offset)
    case IfElse(offset, cond, then, else0) => visitIfElse(offset, cond, then, else0)
    case TryElse(offset, try0, else0) => visitTryElse(offset, try0, else0)

    case Comp(offset, value, first, rest) =>
      Val.Arr(visitComp(first :: rest.toList, Seq(scope)).map(s => Val.Lazy(visitExpr(value)(s, implicitly))))
    case ObjExtend(offset, value, ext) => {
      val original = visitExpr(value).cast[Val.Obj]
      val extension = visitObjBody(ext)
      extension.addSuper(original)
    }
  } catch Error.tryCatch(expr.offset)

  def visitId(offset: Int, value: Int)(implicit scope: ValScope, fileScope: FileScope): Val = {
    val ref = scope.bindings(value)
      .getOrElse(
        Error.fail(
          "Unknown variable " + fileScope.indexNames(value),
          offset
        )
      )

    try ref.force catch Error.tryCatchWrap(offset)
  }

  def visitIfElse(offset: Int,
                  cond: Expr,
                  then: Expr,
                  else0: Option[Expr])
                 (implicit scope: ValScope,
                  fileScope: FileScope): Val = {
    visitExpr(cond) match {
      case Val.True => visitExpr(then)
      case Val.False =>
        else0 match{
          case None => Val.Null
          case Some(v) => visitExpr(v)
        }
      case v => Error.fail("Need boolean, found " + v.prettyName, offset)
    }
  }

  def visitTryElse(offset: Int,
                   try0: Expr,
                   else0: Expr)
                 (implicit scope: ValScope,
                  fileScope: FileScope): Val = {
    try visitExpr(try0, true)
    catch {
      case err: Throwable => visitExpr(else0, true)
    }
  }

  def visitError(offset: Int, value: Expr)
                (implicit scope: ValScope, fileScope: FileScope): Nothing = {
    Error.fail(
      visitExpr(value) match {
        case Val.Str(s) => s
        case r =>
          try Materializer.stringify(r)
          catch Error.tryCatchWrap(offset)
      },
      offset
    )
  }

  def visitUnaryOp(op: UnaryOp.Op, value: Expr)
                  (implicit scope: ValScope, fileScope: FileScope): Val = {
    (op, visitExpr(value)) match {
      case (Expr.UnaryOp.`-`, Val.Num(v)) => Val.Num(-v)
      case (Expr.UnaryOp.`+`, Val.Num(v)) => Val.Num(v)
      case (Expr.UnaryOp.`~`, Val.Num(v)) => Val.Num(~v.toLong)
      case (Expr.UnaryOp.`!`, Val.True) => Val.False
      case (Expr.UnaryOp.`!`, Val.False) => Val.True
    }
  }

  private def visitApply(offset: Int, value: Expr, args: Seq[(Option[String], Expr)])
                        (implicit scope: ValScope, fileScope: FileScope) = {
    val lhs = visitExpr(value)
    try lhs.cast[Val.Func].apply(
      args.map { case (k, v) => (k, Val.Lazy(visitExpr(v))) },
      fileScope.currentFile.last,
      offset
    )
    catch Error.tryCatchWrap(offset)
  }

  def visitAssert(offset: Int, value: Expr, msg: Option[Expr], returned: Expr)
                 (implicit scope: ValScope, fileScope: FileScope): Val = {
    if (visitExpr(value) != Val.True) {
      msg match {
        case None => Error.fail("Assertion failed", offset)
        case Some(msg) =>
          Error.fail(
            "Assertion failed: " + visitExpr(msg).cast[Val.Str].value,
            offset
          )
      }
    }
    visitExpr(returned)
  }

  private def visitSlice(offset: Int,
                         value: Expr,
                         start: Option[Expr],
                         end: Option[Expr],
                         stride: Option[Expr])
                        (implicit scope: ValScope, fileScope: FileScope)= {
    visitExpr(value) match {
      case Val.Arr(a) =>

        val range =
          start.fold(0)(visitExpr(_).cast[Val.Num].value.toInt) until
            end.fold(a.length)(visitExpr(_).cast[Val.Num].value.toInt) by
            stride.fold(1)(visitExpr(_).cast[Val.Num].value.toInt)
        Val.Arr(range.dropWhile(_ < 0).takeWhile(_ < a.length).map(a))
      case Val.Str(s) =>
        val range =
          start.fold(0)(visitExpr(_).cast[Val.Num].value.toInt) until
            end.fold(s.length)(visitExpr(_).cast[Val.Num].value.toInt) by
            stride.fold(1)(visitExpr(_).cast[Val.Num].value.toInt)
        Val.Str(range.dropWhile(_ < 0).takeWhile(_ < s.length).map(s).mkString)
      case x => Error.fail("Can only slice array or string, not " + x.prettyName, offset)
    }
  }

  def visitLookup(offset: Int, value: Expr, index: Expr)
                 (implicit scope: ValScope, fileScope: FileScope): Val = {
    if (value.isInstanceOf[Super]) {
      val key = visitExpr(index).cast[Val.Str]
      scope.super0.getOrElse(scope.self0.getOrElse(Error.fail("Cannot use `super` outside an object", offset))).value(key.value, offset)
    } else (visitExpr(value), visitExpr(index)) match {
      case (v: Val.Arr, i: Val.Num) =>
        if (i.value > v.value.length) Error.fail(s"array bounds error: ${i.value} not within [0, ${v.value.length})", offset)
        val int = i.value.toInt
        if (int != i.value) Error.fail("array index was not integer: " + i.value, offset)
        try v.value(int).force
        catch Error.tryCatchWrap(offset)
      case (v: Val.Str, i: Val.Num) => Val.Str(new String(Array(v.value(i.value.toInt))))
      case (v: Val.Obj, i: Val.Str) =>
        val ref = v.value(i.value, offset)
        try ref
        catch Error.tryCatchWrap(offset)
      case (lhs, rhs) =>
        Error.fail(s"attempted to index a ${lhs.prettyName} with ${rhs.prettyName}", offset)
    }
  }

  def visitSelect(offset: Int, value: Expr, name: String, tryCatch: Boolean)
                 (implicit scope: ValScope, fileScope: FileScope): Val = {
    if (value.isInstanceOf[Super]) {
      scope.super0
        .getOrElse(Error.fail("Cannot use `super` outside an object", offset))
        .value(name, offset, scope.self0.get, defaultValue)
    } else visitExpr(value) match {
      case obj: Val.Obj => if (!tryCatch) obj.value(name, offset, obj, defaultValue) else Error.fail(s"Object does not have a field ${name}", offset)
      case r => if (defaultValue != null && !tryCatch) Materializer.reverse(defaultValue) else Error.fail(s"attempted to index a ${r.prettyName} with string ${name}", offset)
    }
  }

  def visitImportStr(offset: Int, value: String)(implicit scope: ValScope, fileScope: FileScope) = {
    val (p, str) = resolveImport(value, offset)
    Val.Str(cachedImportedStrings.getOrElseUpdate(p, str))
  }

  def visitImport(offset: Int, value: String)(implicit scope: ValScope, fileScope: FileScope) = {
    val (p, str) = resolveImport(value, offset)
    loadedFileContents(p) = str
    cachedImports.getOrElseUpdate(
      p,
      {
        val (doc, nameIndices) = parseCache.getOrElseUpdate(
          str,
          fastparse.parse(str, Parser.document(_))
        ) match {
          case Parsed.Success((doc, nameIndices), _) => (doc, nameIndices)
          case f @ Parsed.Failure(l, i, e) =>
            Error.fail(
              "Imported file " + pprint.Util.literalize(value) +
                " had Parse error. " + f.trace().msg,
              offset
            )
        }
        val newFileScope = new FileScope(p, nameIndices)
        try visitExpr(doc)(Std.scope(nameIndices.size), newFileScope)
        catch Error.tryCatchWrap(offset)
      }
    )
  }

  def resolveImport(value: String, offset: Int)
                   (implicit scope: ValScope, fileScope: FileScope): (Path, String) = {
    importer(fileScope.currentFile.parent(), value)
      .getOrElse(
        Error.fail(
          "Couldn't import file: " + pprint.Util.literalize(value),
          offset
        )
      )
  }

  def visitBinaryOp(offset: Int, lhs: Expr, op: BinaryOp.Op, rhs: Expr)
                   (implicit scope: ValScope, fileScope: FileScope) = {
    op match {
      case Expr.BinaryOp.`default` =>
                try visitExpr(lhs, true)
                catch { case e: Error => visitExpr(rhs, true) }
      // && and || are handled specially because unlike the other operators,
      // these short-circuit during evaluation in some cases when the LHS is known.
      case Expr.BinaryOp.`&&` | Expr.BinaryOp.`||` =>
        (visitExpr(lhs), op) match {
          case (lhs, Expr.BinaryOp.`&&`) =>
            lhs match{
              case Val.True =>
                visitExpr(rhs) match{
                  case b: Val.Bool => b
                  case unknown =>
                    Error.fail(s"binary operator && does not operate on ${unknown.prettyName}s.", offset)
                }
              case Val.False => Val.False
              case unknown =>
                Error.fail(s"binary operator && does not operate on ${unknown.prettyName}s.", offset)
            }
          case (lhs, Expr.BinaryOp.`||`) =>
            lhs match{
              case Val.True => Val.True
              case Val.False =>
                visitExpr(rhs) match{
                  case b: Val.Bool => b
                  case unknown =>
                    Error.fail(s"binary operator || does not operate on ${unknown.prettyName}s.", offset)
                }
              case unknown =>
                Error.fail(s"binary operator || does not operate on ${unknown.prettyName}s.", offset)
            }
          case _ => visitExpr(rhs)

        }
      case _ =>
        (visitExpr(lhs), op, visitExpr(rhs)) match {
          case (Val.Num(l), Expr.BinaryOp.`*`, Val.Num(r)) => Val.Num(l * r)
          case (Val.Num(l), Expr.BinaryOp.`/`, Val.Num(r)) =>
            if (r == 0) Error.fail("division by zero", offset)
            Val.Num(l / r)
          case (Val.Num(l), Expr.BinaryOp.`%`, Val.Num(r)) => Val.Num(l % r)
          case (Val.Num(l), Expr.BinaryOp.`+`, Val.Num(r)) => Val.Num(l + r)
          case (Val.Str(l), Expr.BinaryOp.`%`, r) =>
            try Val.Str(Format.format(l, r, offset))
            catch Error.tryCatchWrap(offset)

          case (Val.Str(l), Expr.BinaryOp.`+`, Val.Str(r)) => Val.Str(l + r)
          case (Val.Str(l), Expr.BinaryOp.`<`, Val.Str(r)) => Val.bool(l < r)
          case (Val.Str(l), Expr.BinaryOp.`>`, Val.Str(r)) => Val.bool(l > r)
          case (Val.Str(l), Expr.BinaryOp.`<=`, Val.Str(r)) => Val.bool(l <= r)
          case (Val.Str(l), Expr.BinaryOp.`>=`, Val.Str(r)) => Val.bool(l >= r)
          case (Val.Str(l), Expr.BinaryOp.`+`, r) =>
            try Val.Str(l + Materializer.stringify(r))
            catch Error.tryCatchWrap(offset)
          case (l, Expr.BinaryOp.`+`, Val.Str(r)) =>
            try Val.Str(Materializer.stringify(l) + r)
            catch Error.tryCatchWrap(offset)
          case (Val.Num(l), Expr.BinaryOp.`-`, Val.Num(r)) => Val.Num(l - r)
          case (Val.Num(l), Expr.BinaryOp.`<<`, Val.Num(r)) => Val.Num(l.toLong << r.toLong)
          case (Val.Num(l), Expr.BinaryOp.`>>`, Val.Num(r)) => Val.Num(l.toLong >> r.toLong)
          case (Val.Num(l), Expr.BinaryOp.`<`, Val.Num(r)) => Val.bool(l < r)
          case (Val.Num(l), Expr.BinaryOp.`>`, Val.Num(r)) => Val.bool(l > r)
          case (Val.Num(l), Expr.BinaryOp.`<=`, Val.Num(r)) => Val.bool(l <= r)
          case (Val.Num(l), Expr.BinaryOp.`>=`, Val.Num(r)) => Val.bool(l >= r)
          case (l, Expr.BinaryOp.`==`, r) =>
            if (l.isInstanceOf[Val.Func] && r.isInstanceOf[Val.Func]) {
              Error.fail("cannot test equality of functions", offset)
            }
            try Val.bool(Materializer(l) == Materializer(r))
            catch Error.tryCatchWrap(offset)
          case (l, Expr.BinaryOp.`!=`, r) =>
            if (l.isInstanceOf[Val.Func] && r.isInstanceOf[Val.Func]) {
              Error.fail("cannot test equality of functions", offset)
            }
            try Val.bool(Materializer(l) != Materializer(r))
            catch Error.tryCatchWrap(offset)
          case (Val.Str(l), Expr.BinaryOp.`in`, o: Val.Obj) => Val.bool(o.containsKey(l))
          case (Val.Num(l), Expr.BinaryOp.`&`, Val.Num(r)) => Val.Num(l.toLong & r.toLong)
          case (Val.Num(l), Expr.BinaryOp.`^`, Val.Num(r)) => Val.Num(l.toLong ^ r.toLong)
          case (Val.Num(l), Expr.BinaryOp.`|`, Val.Num(r)) => Val.Num(l.toLong | r.toLong)
          case (l: Val.Obj, Expr.BinaryOp.`+`, r: Val.Obj) => r.addSuper(l)
          case (Val.Arr(l), Expr.BinaryOp.`+`, Val.Arr(r)) => Val.Arr(l ++ r)
          case (l, op, r) =>
            Error.fail(s"Unknown binary operation: ${l.prettyName} $op ${r.prettyName}", offset)
        }
    }
  }

  def visitFieldName(fieldName: FieldName, offset: Int)
                    (implicit scope: ValScope, fileScope: FileScope) = {
    fieldName match{
      case FieldName.Fixed(s) => Some(s)
      case FieldName.Dyn(k) => visitExpr(k) match{
        case Val.Str(k1) => Some(k1)
        case Val.Null => None
        case x => Error.fail(
          s"Field name must be string or null, not ${x.prettyName}",
          offset
        )
      }
    }
  }

  def visitMethod(rhs: Expr, params: Params, outerOffset: Int)
                 (implicit scope: ValScope, fileScope: FileScope) = {
    Val.Func(
      Some(scope -> fileScope),
      params,
      (s, _, _, fs, _) => visitExpr(rhs)(s, fs),
      (default, s, e) => visitExpr(default)(s, fileScope)
    )
  }

  def visitBindings(bindings: Iterator[Bind], scope: (Option[Val.Obj], Option[Val.Obj]) => ValScope)
                   (implicit fileScope: FileScope)= {
    bindings.map{ b: Bind =>
      b.args match{
        case None =>
          (
            b.name,
            (self: Option[Val.Obj], sup: Option[Val.Obj]) =>
              Val.Lazy(visitExpr(b.rhs)(scope(self, sup), implicitly))
          )
        case Some(argSpec) =>
          (
            b.name,
            (self: Option[Val.Obj], sup: Option[Val.Obj]) =>
              Val.Lazy(visitMethod(b.rhs, argSpec, b.offset)(scope(self, sup), implicitly))

          )
      }
    }
  }

  def visitObjBody(b: ObjBody)(implicit scope: ValScope, fileScope: FileScope): Val.Obj = b match{
    case ObjBody.MemberList(value) =>
      var asserting: Boolean = false
      def assertions(self: Val.Obj) = if (!asserting) {
        asserting = true
        val newScope: ValScope = makeNewScope(Some(self), self.getSuper)

        value.collect {
          case Member.AssertStmt(value, msg) =>

            if (visitExpr(value)(newScope, fileScope) != Val.True) {
              msg match{
                case None => Error.fail("Assertion failed", value.offset)
                case Some(msg) =>
                  Error.fail(
                    "Assertion failed: " + visitExpr(msg)(newScope, implicitly).cast[Val.Str].value,
                    value.offset
                  )
              }
            }
        }
      }

      def makeNewScope(self: Option[Val.Obj], sup: Option[Val.Obj]): ValScope = {
        scope.extend(
          newBindings,
          newDollar = scope.dollar0.orElse(self),
          newSelf = self,
          newSuper = sup
        )
      }

      lazy val newBindings = visitBindings(
        value.iterator.collect{case Member.BindStmt(b) => b},
        (self, sup) => makeNewScope(self, sup)
      ).toArray

      lazy val newSelf: Val.Obj = {
        val builder = mutable.LinkedHashMap.newBuilder[String, Val.Obj.Member]
        value.foreach {
          case Member.Field(offset, fieldName, plus, None, sep, rhs) =>
            visitFieldName(fieldName, offset).map(_ -> Val.Obj.Member(plus, sep, (self: Val.Obj, sup: Option[Val.Obj], _, _) => {
              assertions(self)
              visitExpr(rhs)(makeNewScope(Some(self), sup), implicitly)
            })).foreach(builder.+=)
          case Member.Field(offset, fieldName, false, Some(argSpec), sep, rhs) =>
            visitFieldName(fieldName, offset).map(_ -> Val.Obj.Member(false, sep, (self: Val.Obj, sup: Option[Val.Obj], _, _) => {
              assertions(self)
              visitMethod(rhs, argSpec, offset)(makeNewScope(Some(self), sup), implicitly)
            })).foreach(builder.+=)
          case _: Member.BindStmt => // do nothing
          case _: Member.AssertStmt => // do nothing
        }

        new Val.Obj(builder.result(), self => assertions(self), None)
      }
      newSelf

    case ObjBody.ObjComp(preLocals, key, value, postLocals, first, rest) =>
      lazy val compScope: ValScope = scope.extend(
        newSuper = None
      )

      lazy val newSelf: Val.Obj = {
        val builder = mutable.LinkedHashMap.newBuilder[String, Val.Obj.Member]
        for(s <- visitComp(first :: rest.toList, Seq(compScope))){
          lazy val newScope: ValScope = s.extend(
            newBindings,
            newDollar = scope.dollar0.orElse(Some(newSelf)),
            newSelf = Some(newSelf),
            newSuper = None
          )

          lazy val newBindings = visitBindings(
            (preLocals.iterator ++ postLocals).collect{ case Member.BindStmt(b) => b},
            (self, sup) => newScope
          ).toArray

          visitExpr(key)(s, implicitly) match {
            case Val.Str(k) =>
              builder += (k -> Val.Obj.Member(false, Visibility.Normal, (self: Val.Obj, sup: Option[Val.Obj], _, _) =>
                visitExpr(value)(
                  s.extend(
                    newBindings,
                    newDollar = Some(s.dollar0.getOrElse(self)),
                    newSelf = Some(self),
                  ),
                  implicitly
                )
              ))
            case Val.Null => // do nothing
          }
        }
        new Val.Obj(builder.result(), _ => (), None)
      }

      newSelf
  }

  def visitComp(f: List[CompSpec], scopes: Seq[ValScope])
               (implicit fileScope: FileScope): Seq[ValScope] = f match{
    case ForSpec(offset, name, expr) :: rest =>
      visitComp(
        rest,
        for{
          s <- scopes
          e <- visitExpr(expr)(s, implicitly) match{
            case Val.Arr(value) => value
            case r => Error.fail(
              "In comprehension, can only iterate over array, not " + r.prettyName,
              expr.offset
            )
          }
        } yield s.extend(Seq(name -> ((self: Option[Val.Obj], sup: Option[Val.Obj]) => e)))
      )
    case IfSpec(offset, expr) :: rest =>
      visitComp(rest, scopes.filter(visitExpr(expr)(_, implicitly) match {
        case Val.True => true
        case Val.False => false
        case other => Error.fail(
          "Condition must be boolean, got " + other.prettyName,
          expr.offset
        )
      }))
    case Nil => scopes
  }
}





© 2015 - 2025 Weber Informatics LLC | Privacy Policy