Skip to content

Commit 11927e8

Browse files
authored
chore: Introduce Eval trait. (#607)
Motivation: Introduce `Eval` trait, make `Lazy` trait entends `Eval`.
1 parent 5ff48cd commit 11927e8

26 files changed

Lines changed: 843 additions & 830 deletions

readme.md

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -207,26 +207,46 @@ line which is run by all of these is defined in
207207
`MainBenchmark.mainArgs`. You need to change it to point to a suitable input
208208
before running a benchmark or the profiler.
209209

210-
## Laziness
210+
## Laziness and Evaluation Model
211211

212212
The Jsonnet language is _lazy_: expressions don't get evaluated unless
213213
their value is needed, and thus even erroneous expressions do not cause
214-
a failure if un-used. This is represented in the Sjsonnet codebase by
215-
`sjsonnet.Lazy`: a wrapper type that encapsulates an arbitrary
216-
computation that returns a `sjsonnet.Val`.
214+
a failure if un-used.
217215
218-
`sjsonnet.Lazy` is used in several places, representing where
219-
laziness is present in the language:
216+
Sjsonnet models this with a flat type hierarchy rooted at `sjsonnet.Eval`:
220217
221-
- Inside `sjsonnet.Scope`, representing local variable name bindings
218+
```
219+
Eval (trait) — common interface: def value: Val
220+
/ \
221+
Lazy Val (sealed abstract class)
222+
(final class) |
223+
Val.Str, Val.Num, Val.Arr, Val.Obj, Val.Func, ...
224+
```
225+
226+
- **`Eval`** is the unified parent trait defining `def value: Val`. All places
227+
that accept either a lazy or an already-computed value use `Eval` as the type.
228+
229+
- **`Lazy`** represents lazy evaluation — a computation that has not yet been
230+
performed. It wraps a `() => Val` closure, caches the result on first access,
231+
and is thread-safe. After the value is computed, the closure reference is
232+
cleared to allow it to be garbage collected.
233+
234+
- **`Val`** represents an already-computed value. It extends `Eval` directly and
235+
implements `value` as simply returning `this`.
236+
237+
The hierarchy is intentionally kept flat (only two direct implementors of `Eval`)
238+
to enable the JVM JIT compiler's bimorphic inlining optimization on the
239+
hot `Eval.value` call site.
240+
241+
`Eval` is used in several places, representing where laziness may be
242+
present in the language:
243+
244+
- Inside `sjsonnet.ValScope`, representing local variable name bindings
222245

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

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

227-
`Val` extends `Lazy` so that an already computed value can be treated as
228-
lazy without having to wrap it.
229-
230250
Unlike [google/jsonnet](https://github.com/google/jsonnet), Sjsonnet caches the
231251
results of lazy computations the first time they are evaluated, avoiding
232252
wasteful re-computation when a value is used more than once.

sjsonnet/src-jvm-native/sjsonnet/stdlib/NativeGzip.scala

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package sjsonnet.stdlib
22

33
import sjsonnet.functions.AbstractFunctionModule
4-
import sjsonnet.{Error, EvalScope, Lazy, Platform, Position, Val}
4+
import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}
55

66
object NativeGzip extends AbstractFunctionModule {
77
def name = "gzip"
88

99
val functions: Seq[(String, Val.Builtin)] = Seq(
1010
"gzip" -> new Val.Builtin1("gzip", "v") {
11-
override def evalRhs(v: Lazy, ev: EvalScope, pos: Position): Val = v.force match {
11+
override def evalRhs(v: Eval, ev: EvalScope, pos: Position): Val = v.value match {
1212
case Val.Str(_, value) => Val.Str(pos, Platform.gzipString(value))
1313
case arr: Val.Arr =>
1414
Val.Str(pos, Platform.gzipBytes(arr.iterator.map(_.cast[Val.Num].asInt.toByte).toArray))

sjsonnet/src-jvm/sjsonnet/stdlib/NativeXz.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package sjsonnet.stdlib
22

33
import sjsonnet.functions.AbstractFunctionModule
4-
import sjsonnet.{Error, EvalScope, Lazy, Platform, Position, Val}
4+
import sjsonnet.{Error, Eval, EvalScope, Platform, Position, Val}
55

66
object NativeXz extends AbstractFunctionModule {
77
def name = "xz"
@@ -13,8 +13,8 @@ object NativeXz extends AbstractFunctionModule {
1313
"compressionLevel",
1414
Array(Val.Null(dummyPos), Val.Null(dummyPos))
1515
) {
16-
override def evalRhs(arg1: Lazy, arg2: Lazy, ev: EvalScope, pos: Position): Val = {
17-
val compressionLevel: Option[Int] = arg2.force match {
16+
override def evalRhs(arg1: Eval, arg2: Eval, ev: EvalScope, pos: Position): Val = {
17+
val compressionLevel: Option[Int] = arg2.value match {
1818
case Val.Null(_) =>
1919
// Use default compression level if the user didn't set one
2020
None
@@ -23,7 +23,7 @@ object NativeXz extends AbstractFunctionModule {
2323
case x =>
2424
Error.fail("Cannot xz encode with compression level " + x.prettyName)
2525
}
26-
arg1.force match {
26+
arg1.value match {
2727
case Val.Str(_, value) => Val.Str(pos, Platform.xzString(value, compressionLevel))
2828
case arr: Val.Arr =>
2929
Val.Str(

sjsonnet/src/sjsonnet/Evaluator.scala

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,14 @@ class Evaluator(
8888
Error.fail("Should not have happened.", e.pos)
8989
}
9090

91-
def visitAsLazy(e: Expr)(implicit scope: ValScope): Lazy = e match {
91+
def visitAsLazy(e: Expr)(implicit scope: ValScope): Eval = e match {
9292
case v: Val => v
93-
case e => new LazyWithComputeFunc(() => visitExpr(e))
93+
case e => new Lazy(() => visitExpr(e))
9494
}
9595

9696
def visitValidId(e: ValidId)(implicit scope: ValScope): Val = {
9797
val ref = scope.bindings(e.nameIdx)
98-
ref.force
98+
ref.value
9999
}
100100

101101
def visitSelect(e: Select)(implicit scope: ValScope): Val = visitExpr(e.value) match {
@@ -116,7 +116,7 @@ class Evaluator(
116116
newScope.bindings(base + i) = b.args match {
117117
case null => visitAsLazy(b.rhs)(newScope)
118118
case argSpec =>
119-
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
119+
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
120120
}
121121
i += 1
122122
}
@@ -311,7 +311,7 @@ class Evaluator(
311311
}
312312

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

317317
if (e.tailstrict) {
@@ -371,10 +371,10 @@ class Evaluator(
371371
if (v.length == 0) Error.fail("array bounds error: array is empty", pos)
372372
if (int >= v.length)
373373
Error.fail(s"array bounds error: $int not within [0, ${v.length})", pos)
374-
v.force(int)
374+
v.value(int)
375375
case (v: Val.Str, i: Val.Num) =>
376376
val int = i.asPositiveInt
377-
val str = v.value
377+
val str = v.str
378378
if (str.isEmpty) Error.fail("string bounds error: string is empty", pos)
379379
val unicodeLength = str.codePointCount(0, str.length)
380380
if (int >= unicodeLength)
@@ -383,7 +383,7 @@ class Evaluator(
383383
val endUtf16 = str.offsetByCodePoints(startUtf16, 1)
384384
Val.Str(pos, str.substring(startUtf16, endUtf16))
385385
case (v: Val.Obj, i: Val.Str) =>
386-
v.value(i.value, pos)
386+
v.value(i.str, pos)
387387
case (lhs, rhs) =>
388388
Error.fail(s"attempted to index a ${lhs.prettyName} with ${rhs.prettyName}", pos)
389389
}
@@ -393,7 +393,7 @@ class Evaluator(
393393
var sup = scope.bindings(e.selfIdx + 1).asInstanceOf[Val.Obj]
394394
val key = visitExpr(e.index).cast[Val.Str]
395395
if (sup == null) sup = scope.bindings(e.selfIdx).asInstanceOf[Val.Obj]
396-
sup.value(key.value, e.pos)
396+
sup.value(key.str, e.pos)
397397
}
398398

399399
def visitImportStr(e: ImportStr): Val.Str =
@@ -460,7 +460,7 @@ class Evaluator(
460460
if (sup == null) Val.False(e.pos)
461461
else {
462462
val key = visitExpr(e.value).cast[Val.Str]
463-
Val.bool(e.pos, sup.containsKey(key.value))
463+
Val.bool(e.pos, sup.containsKey(key.str))
464464
}
465465
}
466466

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

645-
def visitBindings(bindings: Array[Bind], scope: => ValScope): Array[Lazy] = {
646-
val arrF = new Array[Lazy](bindings.length)
645+
def visitBindings(bindings: Array[Bind], scope: => ValScope): Array[Eval] = {
646+
val arrF = new Array[Eval](bindings.length)
647647
var i = 0
648648
while (i < bindings.length) {
649649
val b = bindings(i)
650650
arrF(i) = b.args match {
651651
case null =>
652-
new LazyWithComputeFunc(() => visitExpr(b.rhs)(scope))
652+
new Lazy(() => visitExpr(b.rhs)(scope))
653653
case argSpec =>
654-
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(scope))
654+
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(scope))
655655
}
656656
i += 1
657657
}
@@ -689,7 +689,7 @@ class Evaluator(
689689
case null => Error.fail("Assertion failed", a.value.pos, "Assert")
690690
case msg =>
691691
Error.fail(
692-
"Assertion failed: " + visitExpr(msg)(newScope).cast[Val.Str].value,
692+
"Assertion failed: " + visitExpr(msg)(newScope).cast[Val.Str].str,
693693
a.value.pos,
694694
"Assert"
695695
)
@@ -716,7 +716,7 @@ class Evaluator(
716716
case null =>
717717
visitAsLazy(b.rhs)(newScope)
718718
case argSpec =>
719-
new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
719+
new Lazy(() => visitMethod(b.rhs, argSpec, b.pos)(newScope))
720720
}
721721
i += 1
722722
j += 1
@@ -851,13 +851,13 @@ class Evaluator(
851851
def compare(x: Val, y: Val): Int = (x, y) match {
852852
case (_: Val.Null, _: Val.Null) => 0
853853
case (x: Val.Num, y: Val.Num) => x.asDouble.compareTo(y.asDouble)
854-
case (x: Val.Str, y: Val.Str) => Util.compareStringsByCodepoint(x.value, y.value)
854+
case (x: Val.Str, y: Val.Str) => Util.compareStringsByCodepoint(x.str, y.str)
855855
case (x: Val.Bool, y: Val.Bool) => x.asBoolean.compareTo(y.asBoolean)
856856
case (x: Val.Arr, y: Val.Arr) =>
857857
val len = math.min(x.length, y.length)
858858
var i = 0
859859
while (i < len) {
860-
val cmp = compare(x.force(i), y.force(i))
860+
val cmp = compare(x.value(i), y.value(i))
861861
if (cmp != 0) return cmp
862862
i += 1
863863
}
@@ -871,7 +871,7 @@ class Evaluator(
871871
case _: Val.Null => y.isInstanceOf[Val.Null]
872872
case x: Val.Str =>
873873
y match {
874-
case y: Val.Str => x.value == y.value
874+
case y: Val.Str => x.str == y.str
875875
case _ => false
876876
}
877877
case x: Val.Num =>
@@ -886,7 +886,7 @@ class Evaluator(
886886
if (xlen != y.length) return false
887887
var i = 0
888888
while (i < xlen) {
889-
if (!equal(x.force(i), y.force(i))) return false
889+
if (!equal(x.value(i), y.value(i))) return false
890890
i += 1
891891
}
892892
true
@@ -995,5 +995,5 @@ object Evaluator {
995995
*/
996996
type Logger = (Boolean, String) => Unit
997997
val emptyStringArray = new Array[String](0)
998-
val emptyLazyArray = new Array[Lazy](0)
998+
val emptyLazyArray = new Array[Eval](0)
999999
}

sjsonnet/src/sjsonnet/Format.scala

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ object Format {
100100
val values = values0 match {
101101
case x: Val.Arr => x
102102
case x: Val.Obj => x
103-
case x => Val.Arr(pos, Array[Lazy](x))
103+
case x => Val.Arr(pos, Array[Eval](x))
104104
}
105105
val output = new StringBuilder
106106
output.append(leading)
@@ -121,9 +121,9 @@ object Format {
121121
val raw = formatted.label match {
122122
case None =>
123123
(formatted.widthStar, formatted.precisionStar) match {
124-
case (false, false) => values.cast[Val.Arr].force(i)
124+
case (false, false) => values.cast[Val.Arr].value(i)
125125
case (true, false) =>
126-
val width = values.cast[Val.Arr].force(i)
126+
val width = values.cast[Val.Arr].value(i)
127127
if (!width.isInstanceOf[Val.Num]) {
128128
Error.fail(
129129
"A * was specified at position %d. An integer is expected for a width".format(
@@ -133,9 +133,9 @@ object Format {
133133
}
134134
i += 1
135135
formatted = formatted.updateWithStarValues(Some(width.asInt), None)
136-
values.cast[Val.Arr].force(i)
136+
values.cast[Val.Arr].value(i)
137137
case (false, true) =>
138-
val precision = values.cast[Val.Arr].force(i)
138+
val precision = values.cast[Val.Arr].value(i)
139139
if (!precision.isInstanceOf[Val.Num]) {
140140
Error.fail(
141141
"A * was specified at position %d. An integer is expected for a precision"
@@ -144,9 +144,9 @@ object Format {
144144
}
145145
i += 1
146146
formatted = formatted.updateWithStarValues(None, Some(precision.asInt))
147-
values.cast[Val.Arr].force(i)
147+
values.cast[Val.Arr].value(i)
148148
case (true, true) =>
149-
val width = values.cast[Val.Arr].force(i)
149+
val width = values.cast[Val.Arr].value(i)
150150
if (!width.isInstanceOf[Val.Num]) {
151151
Error.fail(
152152
"A * was specified at position %d. An integer is expected for a width".format(
@@ -155,7 +155,7 @@ object Format {
155155
)
156156
}
157157
i += 1
158-
val precision = values.cast[Val.Arr].force(i)
158+
val precision = values.cast[Val.Arr].value(i)
159159
if (!precision.isInstanceOf[Val.Num]) {
160160
Error.fail(
161161
"A * was specified at position %d. An integer is expected for a precision"
@@ -165,16 +165,16 @@ object Format {
165165
i += 1
166166
formatted =
167167
formatted.updateWithStarValues(Some(width.asInt), Some(precision.asInt))
168-
values.cast[Val.Arr].force(i)
168+
values.cast[Val.Arr].value(i)
169169
}
170170
case Some(key) =>
171171
values match {
172-
case v: Val.Arr => v.force(i)
172+
case v: Val.Arr => v.value(i)
173173
case v: Val.Obj => v.value(key, pos)
174174
case _ => Error.fail("Invalid format values")
175175
}
176176
}
177-
val value = raw.force match {
177+
val value = raw.value match {
178178
case f: Val.Func => Error.fail("Cannot format function value", f)
179179
case r: Val.Arr => Materializer.apply0(r, new Renderer(indent = -1))
180180
case r: Val.Obj => Materializer.apply0(r, new Renderer(indent = -1))
@@ -385,7 +385,7 @@ object Format {
385385

386386
class PartialApplyFmt(fmt: String) extends Val.Builtin1("format", "values") {
387387
val (leading, chunks) = fastparse.parse(fmt, format(_)).get.value
388-
def evalRhs(values0: Lazy, ev: EvalScope, pos: Position): Val =
389-
Val.Str(pos, format(leading, chunks, values0.force, pos)(ev))
388+
def evalRhs(values0: Eval, ev: EvalScope, pos: Position): Val =
389+
Val.Str(pos, format(leading, chunks, values0.value, pos)(ev))
390390
}
391391
}

sjsonnet/src/sjsonnet/Materializer.scala

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ abstract class Materializer {
5050
var i = 0
5151
while (i < xs.length) {
5252
val sub = arrVisitor.subVisitor.asInstanceOf[Visitor[T, T]]
53-
arrVisitor.visitValue(apply0(xs.force(i), sub), -1)
53+
arrVisitor.visitValue(apply0(xs.value(i), sub), -1)
5454
i += 1
5555
}
5656
arrVisitor.visitEnd(-1)
@@ -83,11 +83,11 @@ abstract class Materializer {
8383
case ujson.Str(s) => Val.Str(pos, s)
8484
case ujson.Arr(xs) =>
8585
val len = xs.length
86-
val res = new Array[Lazy](len)
86+
val res = new Array[Eval](len)
8787
var i = 0
8888
while (i < len) {
8989
val x = xs(i)
90-
res(i) = new LazyWithComputeFunc(() => reverse(pos, x))
90+
res(i) = new Lazy(() => reverse(pos, x))
9191
i += 1
9292
}
9393
Val.Arr(pos, res)
@@ -147,7 +147,7 @@ object Materializer extends Materializer {
147147
def storePos(v: Val): Unit = ()
148148

149149
final val emptyStringArray = new Array[String](0)
150-
final val emptyLazyArray = new Array[Lazy](0)
150+
final val emptyLazyArray = new Array[Eval](0)
151151

152152
/**
153153
* Trait for providing custom materialization logic to the Materializer.

0 commit comments

Comments
 (0)