Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,26 +207,46 @@ line which is run by all of these is defined in
`MainBenchmark.mainArgs`. You need to change it to point to a suitable input
before running a benchmark or the profiler.

## Laziness
## Laziness and Evaluation Model

The Jsonnet language is _lazy_: expressions don't get evaluated unless
their value is needed, and thus even erroneous expressions do not cause
a failure if un-used. This is represented in the Sjsonnet codebase by
`sjsonnet.Lazy`: a wrapper type that encapsulates an arbitrary
computation that returns a `sjsonnet.Val`.
a failure if un-used.

`sjsonnet.Lazy` is used in several places, representing where
laziness is present in the language:
Sjsonnet models this with a flat type hierarchy rooted at `sjsonnet.Eval`:

- Inside `sjsonnet.Scope`, representing local variable name bindings
```
Eval (trait) — common interface: def value: Val
/ \
Lazy Val (sealed abstract class)
(final class) |
Val.Str, Val.Num, Val.Arr, Val.Obj, Val.Func, ...
```

- **`Eval`** is the unified parent trait defining `def value: Val`. All places
that accept either a lazy or an already-computed value use `Eval` as the type.

- **`Lazy`** represents lazy evaluation — a computation that has not yet been
performed. It wraps a `() => Val` closure, caches the result on first access,
and is thread-safe. After the value is computed, the closure reference is
cleared to allow it to be garbage collected.

- **`Val`** represents an already-computed value. It extends `Eval` directly and
implements `value` as simply returning `this`.

The hierarchy is intentionally kept flat (only two direct implementors of `Eval`)
to enable the JVM JIT compiler's bimorphic inlining optimization on the
hot `Eval.value` call site.

`Eval` is used in several places, representing where laziness may be
present in the language:

- Inside `sjsonnet.ValScope`, representing local variable name bindings

- Inside `sjsonnet.Val.Arr`, representing the contents of array cells

- Inside `sjsonnet.Val.Obj`, representing the contents of object values

`Val` extends `Lazy` so that an already computed value can be treated as
lazy without having to wrap it.

Unlike [google/jsonnet](https://github.com/google/jsonnet), Sjsonnet caches the
results of lazy computations the first time they are evaluated, avoiding
wasteful re-computation when a value is used more than once.
Expand Down
4 changes: 2 additions & 2 deletions sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package sjsonnet.stdlib

import sjsonnet.functions.AbstractFunctionModule
import sjsonnet.{Error, EvalScope, Lazy, Platform, Position, Val}
import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}

object NativeGzip extends AbstractFunctionModule {
def name = "gzip"

val functions: Seq[(String, Val.Builtin)] = Seq(
"gzip" -> new Val.Builtin1("gzip", "v") {
override def evalRhs(v: Lazy, ev: EvalScope, pos: Position): Val = v.force match {
override def evalRhs(v: Eval, ev: EvalScope, pos: Position): Val = v.value match {
case Val.Str(_, value) => Val.Str(pos, Platform.gzipString(value))
case arr: Val.Arr =>
Val.Str(pos, Platform.gzipBytes(arr.iterator.map(_.cast[Val.Num].asInt.toByte).toArray))
Expand Down
8 changes: 4 additions & 4 deletions sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package sjsonnet.stdlib

import sjsonnet.functions.AbstractFunctionModule
import sjsonnet.{Error, EvalScope, Lazy, Platform, Position, Val}
import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}

object NativeXz extends AbstractFunctionModule {
def name = "xz"
Expand All @@ -13,8 +13,8 @@ object NativeXz extends AbstractFunctionModule {
"compressionLevel",
Array(Val.Null(dummyPos), Val.Null(dummyPos))
) {
override def evalRhs(arg1: Lazy, arg2: Lazy, ev: EvalScope, pos: Position): Val = {
val compressionLevel: Option[Int] = arg2.force match {
override def evalRhs(arg1: Eval, arg2: Eval, ev: EvalScope, pos: Position): Val = {
val compressionLevel: Option[Int] = arg2.value match {
case Val.Null(_) =>
// Use default compression level if the user didn't set one
None
Expand All @@ -23,7 +23,7 @@ object NativeXz extends AbstractFunctionModule {
case x =>
Error.fail("Cannot xz encode with compression level " + x.prettyName)
}
arg1.force match {
arg1.value match {
case Val.Str(_, value) => Val.Str(pos, Platform.xzString(value, compressionLevel))
case arr: Val.Arr =>
Val.Str(
Expand Down
42 changes: 21 additions & 21 deletions sjsonnet/src/sjsonnet/Evaluator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ class Evaluator(
Error.fail("Should not have happened.", e.pos)
}

def visitAsLazy(e: Expr)(implicit scope: ValScope): Lazy = e match {
def visitAsLazy(e: Expr)(implicit scope: ValScope): Eval = e match {
case v: Val => v
case e => new LazyWithComputeFunc(() => visitExpr(e))
case e => new Lazy(() => visitExpr(e))
}

def visitValidId(e: ValidId)(implicit scope: ValScope): Val = {
val ref = scope.bindings(e.nameIdx)
ref.force
ref.value
}

def visitSelect(e: Select)(implicit scope: ValScope): Val = visitExpr(e.value) match {
Expand All @@ -116,7 +116,7 @@ class Evaluator(
newScope.bindings(base + i) = b.args match {
case null => visitAsLazy(b.rhs)(newScope)
case argSpec =>
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
}
i += 1
}
Expand Down Expand Up @@ -311,7 +311,7 @@ class Evaluator(
}

protected def visitApplyBuiltin(e: ApplyBuiltin)(implicit scope: ValScope): Val = {
val arr = new Array[Lazy](e.argExprs.length)
val arr = new Array[Eval](e.argExprs.length)
var idx = 0

if (e.tailstrict) {
Expand Down Expand Up @@ -371,10 +371,10 @@ class Evaluator(
if (v.length == 0) Error.fail("array bounds error: array is empty", pos)
if (int >= v.length)
Error.fail(s"array bounds error: $int not within [0, ${v.length})", pos)
v.force(int)
v.value(int)
case (v: Val.Str, i: Val.Num) =>
val int = i.asPositiveInt
val str = v.value
val str = v.str
if (str.isEmpty) Error.fail("string bounds error: string is empty", pos)
val unicodeLength = str.codePointCount(0, str.length)
if (int >= unicodeLength)
Expand All @@ -383,7 +383,7 @@ class Evaluator(
val endUtf16 = str.offsetByCodePoints(startUtf16, 1)
Val.Str(pos, str.substring(startUtf16, endUtf16))
case (v: Val.Obj, i: Val.Str) =>
v.value(i.value, pos)
v.value(i.str, pos)
case (lhs, rhs) =>
Error.fail(s"attempted to index a ${lhs.prettyName} with ${rhs.prettyName}", pos)
}
Expand All @@ -393,7 +393,7 @@ class Evaluator(
var sup = scope.bindings(e.selfIdx + 1).asInstanceOf[Val.Obj]
val key = visitExpr(e.index).cast[Val.Str]
if (sup == null) sup = scope.bindings(e.selfIdx).asInstanceOf[Val.Obj]
sup.value(key.value, e.pos)
sup.value(key.str, e.pos)
}

def visitImportStr(e: ImportStr): Val.Str =
Expand Down Expand Up @@ -460,7 +460,7 @@ class Evaluator(
if (sup == null) Val.False(e.pos)
else {
val key = visitExpr(e.value).cast[Val.Str]
Val.bool(e.pos, sup.containsKey(key.value))
Val.bool(e.pos, sup.containsKey(key.str))
}
}

Expand Down Expand Up @@ -642,16 +642,16 @@ class Evaluator(
override def evalDefault(expr: Expr, vs: ValScope, es: EvalScope): Val = visitExpr(expr)(vs)
}

def visitBindings(bindings: Array[Bind], scope: => ValScope): Array[Lazy] = {
val arrF = new Array[Lazy](bindings.length)
def visitBindings(bindings: Array[Bind], scope: => ValScope): Array[Eval] = {
val arrF = new Array[Eval](bindings.length)
var i = 0
while (i < bindings.length) {
val b = bindings(i)
arrF(i) = b.args match {
case null =>
new LazyWithComputeFunc(() => visitExpr(b.rhs)(scope))
new Lazy(() => visitExpr(b.rhs)(scope))
case argSpec =>
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(scope))
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(scope))
}
i += 1
}
Expand Down Expand Up @@ -689,7 +689,7 @@ class Evaluator(
case null => Error.fail("Assertion failed", a.value.pos, "Assert")
case msg =>
Error.fail(
"Assertion failed: " + visitExpr(msg)(newScope).cast[Val.Str].value,
"Assertion failed: " + visitExpr(msg)(newScope).cast[Val.Str].str,
a.value.pos,
"Assert"
)
Expand All @@ -716,7 +716,7 @@ class Evaluator(
case null =>
visitAsLazy(b.rhs)(newScope)
case argSpec =>
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
}
i += 1
j += 1
Expand Down Expand Up @@ -851,13 +851,13 @@ class Evaluator(
def compare(x: Val, y: Val): Int = (x, y) match {
case (_: Val.Null, _: Val.Null) => 0
case (x: Val.Num, y: Val.Num) => x.asDouble.compareTo(y.asDouble)
case (x: Val.Str, y: Val.Str) => Util.compareStringsByCodepoint(x.value, y.value)
case (x: Val.Str, y: Val.Str) => Util.compareStringsByCodepoint(x.str, y.str)
case (x: Val.Bool, y: Val.Bool) => x.asBoolean.compareTo(y.asBoolean)
case (x: Val.Arr, y: Val.Arr) =>
val len = math.min(x.length, y.length)
var i = 0
while (i < len) {
val cmp = compare(x.force(i), y.force(i))
val cmp = compare(x.value(i), y.value(i))
if (cmp != 0) return cmp
i += 1
}
Expand All @@ -871,7 +871,7 @@ class Evaluator(
case _: Val.Null => y.isInstanceOf[Val.Null]
case x: Val.Str =>
y match {
case y: Val.Str => x.value == y.value
case y: Val.Str => x.str == y.str
case _ => false
}
case x: Val.Num =>
Expand All @@ -886,7 +886,7 @@ class Evaluator(
if (xlen != y.length) return false
var i = 0
while (i < xlen) {
if (!equal(x.force(i), y.force(i))) return false
if (!equal(x.value(i), y.value(i))) return false
i += 1
}
true
Expand Down Expand Up @@ -995,5 +995,5 @@ object Evaluator {
*/
type Logger = (Boolean, String) => Unit
val emptyStringArray = new Array[String](0)
val emptyLazyArray = new Array[Lazy](0)
val emptyLazyArray = new Array[Eval](0)
}
26 changes: 13 additions & 13 deletions sjsonnet/src/sjsonnet/Format.scala
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ object Format {
val values = values0 match {
case x: Val.Arr => x
case x: Val.Obj => x
case x => Val.Arr(pos, Array[Lazy](x))
case x => Val.Arr(pos, Array[Eval](x))
}
val output = new StringBuilder
output.append(leading)
Expand All @@ -121,9 +121,9 @@ object Format {
val raw = formatted.label match {
case None =>
(formatted.widthStar, formatted.precisionStar) match {
case (false, false) => values.cast[Val.Arr].force(i)
case (false, false) => values.cast[Val.Arr].value(i)
case (true, false) =>
val width = values.cast[Val.Arr].force(i)
val width = values.cast[Val.Arr].value(i)
if (!width.isInstanceOf[Val.Num]) {
Error.fail(
"A * was specified at position %d. An integer is expected for a width".format(
Expand All @@ -133,9 +133,9 @@ object Format {
}
i += 1
formatted = formatted.updateWithStarValues(Some(width.asInt), None)
values.cast[Val.Arr].force(i)
values.cast[Val.Arr].value(i)
case (false, true) =>
val precision = values.cast[Val.Arr].force(i)
val precision = values.cast[Val.Arr].value(i)
if (!precision.isInstanceOf[Val.Num]) {
Error.fail(
"A * was specified at position %d. An integer is expected for a precision"
Expand All @@ -144,9 +144,9 @@ object Format {
}
i += 1
formatted = formatted.updateWithStarValues(None, Some(precision.asInt))
values.cast[Val.Arr].force(i)
values.cast[Val.Arr].value(i)
case (true, true) =>
val width = values.cast[Val.Arr].force(i)
val width = values.cast[Val.Arr].value(i)
if (!width.isInstanceOf[Val.Num]) {
Error.fail(
"A * was specified at position %d. An integer is expected for a width".format(
Expand All @@ -155,7 +155,7 @@ object Format {
)
}
i += 1
val precision = values.cast[Val.Arr].force(i)
val precision = values.cast[Val.Arr].value(i)
if (!precision.isInstanceOf[Val.Num]) {
Error.fail(
"A * was specified at position %d. An integer is expected for a precision"
Expand All @@ -165,16 +165,16 @@ object Format {
i += 1
formatted =
formatted.updateWithStarValues(Some(width.asInt), Some(precision.asInt))
values.cast[Val.Arr].force(i)
values.cast[Val.Arr].value(i)
}
case Some(key) =>
values match {
case v: Val.Arr => v.force(i)
case v: Val.Arr => v.value(i)
case v: Val.Obj => v.value(key, pos)
case _ => Error.fail("Invalid format values")
}
}
val value = raw.force match {
val value = raw.value match {
case f: Val.Func => Error.fail("Cannot format function value", f)
case r: Val.Arr => Materializer.apply0(r, new Renderer(indent = -1))
case r: Val.Obj => Materializer.apply0(r, new Renderer(indent = -1))
Expand Down Expand Up @@ -380,7 +380,7 @@ object Format {

class PartialApplyFmt(fmt: String) extends Val.Builtin1("format", "values") {
val (leading, chunks) = fastparse.parse(fmt, format(_)).get.value
def evalRhs(values0: Lazy, ev: EvalScope, pos: Position): Val =
Val.Str(pos, format(leading, chunks, values0.force, pos)(ev))
def evalRhs(values0: Eval, ev: EvalScope, pos: Position): Val =
Val.Str(pos, format(leading, chunks, values0.value, pos)(ev))
}
}
8 changes: 4 additions & 4 deletions sjsonnet/src/sjsonnet/Materializer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ abstract class Materializer {
var i = 0
while (i < xs.length) {
val sub = arrVisitor.subVisitor.asInstanceOf[Visitor[T, T]]
arrVisitor.visitValue(apply0(xs.force(i), sub), -1)
arrVisitor.visitValue(apply0(xs.value(i), sub), -1)
i += 1
}
arrVisitor.visitEnd(-1)
Expand Down Expand Up @@ -83,11 +83,11 @@ abstract class Materializer {
case ujson.Str(s) => Val.Str(pos, s)
case ujson.Arr(xs) =>
val len = xs.length
val res = new Array[Lazy](len)
val res = new Array[Eval](len)
var i = 0
while (i < len) {
val x = xs(i)
res(i) = new LazyWithComputeFunc(() => reverse(pos, x))
res(i) = new Lazy(() => reverse(pos, x))
i += 1
}
Val.Arr(pos, res)
Expand Down Expand Up @@ -147,7 +147,7 @@ object Materializer extends Materializer {
def storePos(v: Val): Unit = ()

final val emptyStringArray = new Array[String](0)
final val emptyLazyArray = new Array[Lazy](0)
final val emptyLazyArray = new Array[Eval](0)

/**
* Trait for providing custom materialization logic to the Materializer.
Expand Down
Loading
Loading