diff --git a/sjsonnet/src/sjsonnet/Evaluator.scala b/sjsonnet/src/sjsonnet/Evaluator.scala index d9f1d1ea..735c3f59 100644 --- a/sjsonnet/src/sjsonnet/Evaluator.scala +++ b/sjsonnet/src/sjsonnet/Evaluator.scala @@ -80,7 +80,7 @@ class Evaluator(resolver: CachedResolver, def visitAsLazy(e: Expr)(implicit scope: ValScope): Lazy = e match { case v: Val => v - case e => () => visitExpr(e) + case e => new LazyWithComputeFunc(() => visitExpr(e)) } def visitValidId(e: ValidId)(implicit scope: ValScope): Val = { @@ -104,7 +104,7 @@ class Evaluator(resolver: CachedResolver, val b = bindings(i) newScope.bindings(base+i) = b.args match { case null => visitAsLazy(b.rhs)(newScope) - case argSpec => () => visitMethod(b.rhs, argSpec, b.pos)(newScope) + case argSpec => new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope)) } i += 1 } @@ -490,9 +490,9 @@ class Evaluator(resolver: CachedResolver, val b = bindings(i) arrF(i) = b.args match { case null => - (self: Val.Obj, sup: Val.Obj) => () => visitExpr(b.rhs)(scope(self, sup)) + (self: Val.Obj, sup: Val.Obj) => new LazyWithComputeFunc(() => visitExpr(b.rhs)(scope(self, sup))) case argSpec => - (self: Val.Obj, sup: Val.Obj) => () => visitMethod(b.rhs, argSpec, b.pos)(scope(self, sup)) + (self: Val.Obj, sup: Val.Obj) => new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(scope(self, sup))) } i += 1 } @@ -564,7 +564,7 @@ class Evaluator(resolver: CachedResolver, case null => visitAsLazy(b.rhs)(newScope) case argSpec => - () => visitMethod(b.rhs, argSpec, b.pos)(newScope) + new LazyWithComputeFunc(() => visitMethod(b.rhs, argSpec, b.pos)(newScope)) } i += 1 j += 1 diff --git a/sjsonnet/src/sjsonnet/Materializer.scala b/sjsonnet/src/sjsonnet/Materializer.scala index c6bea8f5..31dc9a5e 100644 --- a/sjsonnet/src/sjsonnet/Materializer.scala +++ b/sjsonnet/src/sjsonnet/Materializer.scala @@ -73,7 +73,7 @@ abstract class Materializer { case ujson.Null => Val.Null(pos) case ujson.Num(n) => Val.Num(pos, n) case ujson.Str(s) => Val.Str(pos, s) - case ujson.Arr(xs) => new Val.Arr(pos, xs.map(x => (() => reverse(pos, x)): Lazy).toArray[Lazy]) + case ujson.Arr(xs) => new Val.Arr(pos, xs.map(x => new LazyWithComputeFunc(() => reverse(pos, x))).toArray[Lazy]) case ujson.Obj(xs) => val builder = new java.util.LinkedHashMap[String, Val.Obj.Member] for(x <- xs) { diff --git a/sjsonnet/src/sjsonnet/Val.scala b/sjsonnet/src/sjsonnet/Val.scala index f8f3fd2e..e6aaaa0b 100644 --- a/sjsonnet/src/sjsonnet/Val.scala +++ b/sjsonnet/src/sjsonnet/Val.scala @@ -16,7 +16,7 @@ import scala.reflect.ClassTag * evaluated dictionary values, array contents, or function parameters * are all wrapped in [[Lazy]] and only truly evaluated on-demand */ -abstract class Lazy { +protected abstract class Lazy { protected[this] var cached: Val = null def compute(): Val final def force: Val = { @@ -25,6 +25,24 @@ abstract class Lazy { } } +/** + * Thread-safe implementation that discards the compute function after initialization. + */ +final class LazyWithComputeFunc(@volatile private[this] var computeFunc: () => Val) extends Lazy { + def compute(): Val = { + val f = computeFunc + if (f != null) { // we won the race to initialize + val result = f() + cached = result + computeFunc = null // allow closure to be GC'd + } + // else: we lost the race to compute, but `cached` is already set and + // is visible in this thread due to the volatile read and writes via + // piggybacking; see https://stackoverflow.com/a/8769692 for background + cached + } +} + /** * [[Val]]s represented Jsonnet values that are the result of evaluating * a Jsonnet program. The [[Val]] data structure is essentially a JSON tree, @@ -395,7 +413,7 @@ object Val{ if(argVals(j) == null) { val default = params.defaultExprs(i) if(default != null) { - argVals(j) = () => evalDefault(default, newScope, ev) + argVals(j) = new LazyWithComputeFunc(() => evalDefault(default, newScope, ev)) } else { if(missing == null) missing = new ArrayBuffer missing.+=(params.names(i))