From 741b394b0dee21f0e0f76fd8e394484059db36a3 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 6 Apr 2023 01:46:08 +0200 Subject: [PATCH] Suspended atom fields are evaluated only once (#6151) Implements #6134. # Important Notes One can define lazy atom fields as: ```haskell type Lazy Value ~x ~y ``` the evaluation of the `x` and `y` fields is then delayed until they are needed. The evaluation happens once. Then the computed value is kept in the atom for further use. --- CHANGELOG.md | 2 + .../Base/0.0.0-dev/src/Runtime/Lazy.enso | 80 ----------- .../src/Internal/SQL_Type_Reference.enso | 9 +- .../benchmarks/semantic/ListBenchmarks.java | 39 ++++- .../callable/atom/AtomConstructor.java | 2 +- .../callable/atom/unboxing/Layout.java | 38 +++-- .../unboxing/SuspendedFieldGetterNode.java | 65 +++++++++ .../interpreter/test/LazyAtomFieldTest.java | 135 ++++++++++++++++++ test/Tests/src/Runtime/Lazy_Spec.enso | 6 +- 9 files changed, 274 insertions(+), 102 deletions(-) delete mode 100644 distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java create mode 100644 engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 715210590afa..9c2e80d6b8e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -670,6 +670,7 @@ - [Don't install Python component on Windows][5900] - [Detect potential name conflicts between exported types and FQNs][5966] - [Ensure calls involving warnings remain instrumented][6067] +- [One can define lazy atom fields][6151] [3227]: https://github.com/enso-org/enso/pull/3227 [3248]: https://github.com/enso-org/enso/pull/3248 @@ -773,6 +774,7 @@ [5900]: https://github.com/enso-org/enso/pull/5900 [5966]: https://github.com/enso-org/enso/pull/5966 [6067]: https://github.com/enso-org/enso/pull/6067 +[6151]: https://github.com/enso-org/enso/pull/6151 # Enso 2.0.0-alpha.18 (2021-10-12) diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso deleted file mode 100644 index f34415fcc35e..000000000000 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso +++ /dev/null @@ -1,80 +0,0 @@ -import project.Any.Any -import project.Error.Error -import project.Nothing.Nothing -import project.Panic.Caught_Panic -import project.Panic.Panic -import project.Runtime.Ref.Ref - -## PRIVATE - Holds a value that is computed on first access. -type Lazy - ## PRIVATE - Lazy (cached_ref : Ref) (builder : Nothing -> Any) - - ## PRIVATE - Eager (value : Any) - - ## PRIVATE - ADVANCED - Creates a new lazy value. - new : Any -> Lazy - new ~lazy_computation = - builder _ = lazy_computation - cached_ref = Ref.new Lazy_Not_Computed_Mark - Lazy.Lazy cached_ref builder - - ## PRIVATE - ADVANCED - Creates a pre-computed lazy value. - This can be useful if a value needs to admit the Lazy type API, but is - known beforehand. - new_eager value = Lazy.Eager value - - ## Returns the stored value. - - The value will be computed on first access and cached. - get : Any - get self = case self of - Lazy.Lazy cached_ref builder -> case cached_ref.get of - Lazy_Not_Computed_Mark -> - cached_value = Cached_Value.freeze builder - cached_ref.put cached_value - cached_value.get - cached_value -> cached_value.get - Lazy.Eager value -> value - -## PRIVATE - This is a special value that should never be returned from a lazy computation - as it will prevent the lazy value from being cached. -type Lazy_Not_Computed_Mark - -## PRIVATE -type Cached_Value - ## PRIVATE - Value value - - ## PRIVATE - Error error - - ## PRIVATE - Panic (caught_panic : Caught_Panic) - - ## PRIVATE - Accesses the cached value as if it was just computed - any stored errors - or panics will be propagated. - get : Any - get self = case self of - Cached_Value.Value value -> value - Cached_Value.Error error -> Error.throw error - Cached_Value.Panic caught_panic -> Panic.throw caught_panic - - ## PRIVATE - Runs the provided `builder` with a `Nothing` argument, handling any - errors or panics and saving them as a `Cached_Value`. - freeze : (Nothing -> Any) -> Cached_Value - freeze builder = - save_panic caught_panic = Cached_Value.Panic caught_panic - Panic.catch Any handler=save_panic <| - result = Cached_Value.Value (builder Nothing) - result.catch Any dataflow_error-> - Cached_Value.Error dataflow_error diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso index 11e02d9b82ac..dd14df7b1a05 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso @@ -1,6 +1,5 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_State.Illegal_State -import Standard.Base.Runtime.Lazy.Lazy import project.Connection.Connection.Connection import project.Data.SQL_Type.SQL_Type @@ -14,7 +13,7 @@ type SQL_Type_Reference Since fetching this type requires querying the database, it is computed lazily and cached. - Computed_By_Database (lazy_ref : Lazy) + Computed_By_Database (~lazy_ref : SQL_Type) ## Refers to an SQL type that is overridden by the dialect's type system. Overridden (value : SQL_Type) @@ -25,7 +24,7 @@ type SQL_Type_Reference This may perform a database query on first access. get : SQL_Type get self = case self of - SQL_Type_Reference.Computed_By_Database lazy_ref -> lazy_ref.get + SQL_Type_Reference.Computed_By_Database lazy_ref -> lazy_ref SQL_Type_Reference.Overridden value -> value ## PRIVATE @@ -51,7 +50,7 @@ type SQL_Type_Reference columns = connection.jdbc_connection.fetch_columns statement statement_setter only_column = columns.first only_column.second - SQL_Type_Reference.Computed_By_Database (Lazy.new do_fetch) + SQL_Type_Reference.Computed_By_Database do_fetch ## PRIVATE Creates a new `SQL_Type_Reference` that should never be used. @@ -61,7 +60,7 @@ type SQL_Type_Reference null = getter = Error.throw (Illegal_State.Error "Getting the SQL_Type from SQL_Type_Reference.null is not allowed. This indicates a bug in the Database library.") - SQL_Type_Reference.Computed_By_Database (Lazy.new getter) + SQL_Type_Reference.Computed_By_Database getter ## PRIVATE Turns this reference into a type override. diff --git a/engine/runtime/src/bench/java/org/enso/interpreter/bench/benchmarks/semantic/ListBenchmarks.java b/engine/runtime/src/bench/java/org/enso/interpreter/bench/benchmarks/semantic/ListBenchmarks.java index 0ac40ed24f1f..ef8b748ab812 100644 --- a/engine/runtime/src/bench/java/org/enso/interpreter/bench/benchmarks/semantic/ListBenchmarks.java +++ b/engine/runtime/src/bench/java/org/enso/interpreter/bench/benchmarks/semantic/ListBenchmarks.java @@ -51,8 +51,25 @@ public void initializeBenchmark(BenchmarkParams params) throws Exception { from Standard.Base.Data.List.List import Cons, Nil import Standard.Base.IO + type Lenivy + Nic + Hlava ~x ~xs + + map self fn = case self of + Lenivy.Nic -> Lenivy.Nic + Lenivy.Hlava x xs -> Lenivy.Hlava (fn x) (xs.map fn) + plus_one list = list.map (x -> x + 1) + leniva_suma list acc = case list of + Lenivy.Nic -> acc + Lenivy.Hlava x xs -> @Tail_Call leniva_suma xs acc+x + + lenivy_generator n = + go x v l = if x > n then l else + @Tail_Call go x+1 v+1 (Lenivy.Hlava v l) + go 1 1 Lenivy.Nic + sum list acc = case list of Nil -> acc @@ -69,15 +86,22 @@ public void initializeBenchmark(BenchmarkParams params) throws Exception { this.self = module.invokeMember("get_associated_type"); Function getMethod = (name) -> module.invokeMember("get_method", self, name); - Value longList = getMethod.apply("generator").execute(self, LENGTH_OF_EXPERIMENT); - this.plusOne = getMethod.apply("plus_one"); - this.sum = getMethod.apply("sum"); switch (benchmarkName) { case "mapOverList": { - this.list = longList; - this.oldSum = sum.execute(self, longList, 0); + this.list = getMethod.apply("generator").execute(self, LENGTH_OF_EXPERIMENT); + this.sum = getMethod.apply("sum"); + this.oldSum = sum.execute(self, this.list, 0); + if (!this.oldSum.fitsInLong()) { + throw new AssertionError("Expecting a number " + this.oldSum); + } + break; + } + case "mapOverLazyList": { + this.list = getMethod.apply("lenivy_generator").execute(self, LENGTH_OF_EXPERIMENT); + this.sum = getMethod.apply("leniva_suma"); + this.oldSum = sum.execute(self, this.list, 0); if (!this.oldSum.fitsInLong()) { throw new AssertionError("Expecting a number " + this.oldSum); } @@ -93,6 +117,11 @@ public void mapOverList(Blackhole matter) { performBenchmark(matter); } + @Benchmark + public void mapOverLazyList(Blackhole matter) { + performBenchmark(matter); + } + private void performBenchmark(Blackhole hole) throws AssertionError { var newList = plusOne.execute(self, list); var newSum = sum.execute(self, newList, 0); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java index 61565bc35055..13886479f87c 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/AtomConstructor.java @@ -123,7 +123,7 @@ public AtomConstructor initializeFields( cachedInstance = null; } if (Layout.isAritySupported(args.length)) { - boxedLayout = Layout.create(args.length, 0); + boxedLayout = Layout.create(args.length, 0, args); } this.constructorFunction = buildConstructorFunction(language, localScope, assignments, varReads, annotations, args); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/Layout.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/Layout.java index fe9ecd89dd57..f3ee0397e0c0 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/Layout.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/Layout.java @@ -6,6 +6,7 @@ import org.enso.interpreter.dsl.atom.LayoutSpec; import org.enso.interpreter.node.expression.atom.InstantiateNode; import org.enso.interpreter.runtime.EnsoContext; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; /** @@ -54,6 +55,7 @@ public static int countLongs(long flags) { private final @CompilerDirectives.CompilationFinal(dimensions = 1) UnboxingAtom.FieldGetterNode[] uncachedFieldGetters; + private final @CompilerDirectives.CompilationFinal(dimensions = 1) ArgumentDefinition[] args; private final @CompilerDirectives.CompilationFinal(dimensions = 1) NodeFactory< ? extends UnboxingAtom.FieldSetterNode>[] fieldSetterFactories; @@ -69,16 +71,14 @@ public Layout( int[] fieldToStorage, NodeFactory[] fieldGetterFactories, NodeFactory[] fieldSetterFactories, - NodeFactory instantiatorFactory) { + NodeFactory instantiatorFactory, + ArgumentDefinition[] args) { + this.args = args; this.inputFlags = inputFlags; this.fieldToStorage = fieldToStorage; this.instantiatorFactory = instantiatorFactory; this.fieldGetterFactories = fieldGetterFactories; this.uncachedFieldGetters = new UnboxingAtom.FieldGetterNode[fieldGetterFactories.length]; - for (int i = 0; i < fieldGetterFactories.length; i++) { - this.uncachedFieldGetters[i] = fieldGetterFactories[i].getUncachedInstance(); - assert this.uncachedFieldGetters[i] != null; - } this.fieldSetterFactories = fieldSetterFactories; this.uncachedFieldSetters = new UnboxingAtom.FieldSetterNode[fieldSetterFactories.length]; for (int i = 0; i < fieldSetterFactories.length; i++) { @@ -86,6 +86,15 @@ public Layout( this.uncachedFieldSetters[i] = fieldSetterFactories[i].getUncachedInstance(); } } + for (int i = 0; i < fieldGetterFactories.length; i++) { + this.uncachedFieldGetters[i] = fieldGetterFactories[i].getUncachedInstance(); + assert this.uncachedFieldGetters[i] != null; + if (args[i].isSuspended()) { + this.uncachedFieldGetters[i] = + SuspendedFieldGetterNode.build( + this.uncachedFieldGetters[i], this.uncachedFieldSetters[i]); + } + } } public static boolean isAritySupported(int arity) { @@ -98,7 +107,7 @@ public static boolean isAritySupported(int arity) { * factories for getters, setters and instantiators. */ @SuppressWarnings("unchecked") - public static Layout create(int arity, long typeFlags) { + public static Layout create(int arity, long typeFlags, ArgumentDefinition[] args) { if (arity > 32) { throw new IllegalArgumentException("Too many fields in unboxed atom"); } @@ -137,7 +146,7 @@ public static Layout create(int arity, long typeFlags) { var instantiatorFactory = LayoutFactory.getInstantiatorNodeFactory(numUnboxed, numBoxed); return new Layout( - typeFlags, fieldToStorage, getterFactories, setterFactories, instantiatorFactory); + typeFlags, fieldToStorage, getterFactories, setterFactories, instantiatorFactory, args); } public UnboxingAtom.FieldGetterNode[] getUncachedFieldGetters() { @@ -148,6 +157,10 @@ public UnboxingAtom.FieldGetterNode[] buildGetters() { var getters = new UnboxingAtom.FieldGetterNode[fieldGetterFactories.length]; for (int i = 0; i < fieldGetterFactories.length; i++) { getters[i] = fieldGetterFactories[i].createNode(); + if (args[i].isSuspended()) { + var setterOrNull = buildSetter(i); + getters[i] = SuspendedFieldGetterNode.build(getters[i], setterOrNull); + } } return getters; } @@ -157,7 +170,11 @@ public UnboxingAtom.FieldGetterNode getUncachedFieldGetter(int index) { } public UnboxingAtom.FieldGetterNode buildGetter(int index) { - return fieldGetterFactories[index].createNode(); + var node = fieldGetterFactories[index].createNode(); + if (args[index].isSuspended()) { + node = SuspendedFieldGetterNode.build(node, buildSetter(index)); + } + return node; } public UnboxingAtom.FieldSetterNode getUncachedFieldSetter(int index) { @@ -165,7 +182,8 @@ public UnboxingAtom.FieldSetterNode getUncachedFieldSetter(int index) { } public UnboxingAtom.FieldSetterNode buildSetter(int index) { - return fieldSetterFactories[index].createNode(); + var fieldSetterFactory = fieldSetterFactories[index]; + return fieldSetterFactory == null ? null : fieldSetterFactory.createNode(); } public boolean isDoubleAt(int fieldIndex) { @@ -233,7 +251,7 @@ public Object execute(Object[] arguments) { if (layouts.length == this.unboxedLayouts.length) { // Layouts stored in this node are probably up-to-date; create a new one and try to // register it. - var newLayout = Layout.create(arity, flags); + var newLayout = Layout.create(arity, flags, boxedLayout.layout.args); constructor.atomicallyAddLayout(newLayout, this.unboxedLayouts.length); } updateFromConstructor(); diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java new file mode 100644 index 000000000000..5760f8515892 --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java @@ -0,0 +1,65 @@ +package org.enso.interpreter.runtime.callable.atom.unboxing; + +import com.oracle.truffle.api.exception.AbstractTruffleException; +import com.oracle.truffle.api.interop.TruffleObject; +import com.oracle.truffle.api.nodes.Node; +import org.enso.interpreter.node.callable.InvokeCallableNode; +import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode; +import org.enso.interpreter.runtime.EnsoContext; +import org.enso.interpreter.runtime.callable.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.atom.Atom; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.state.State; + +/** + * Getter node that reads a field value. If the value is a thunk the node + * evaluates it and replaces the original lazy value with the new value. + */ +final class SuspendedFieldGetterNode extends UnboxingAtom.FieldGetterNode { + @Node.Child + private UnboxingAtom.FieldSetterNode set; + @Node.Child + private UnboxingAtom.FieldGetterNode get; + @Node.Child + private InvokeFunctionNode invoke = InvokeFunctionNode.build( + new CallArgumentInfo[0], InvokeCallableNode.DefaultsExecutionMode.EXECUTE, InvokeCallableNode.ArgumentsExecutionMode.EXECUTE + ); + + private SuspendedFieldGetterNode(UnboxingAtom.FieldGetterNode get, UnboxingAtom.FieldSetterNode set) { + this.get = get; + this.set = set; + } + + static UnboxingAtom.FieldGetterNode build(UnboxingAtom.FieldGetterNode get, UnboxingAtom.FieldSetterNode set) { + return new SuspendedFieldGetterNode(get, set); + } + + @Override + public Object execute(Atom atom) { + java.lang.Object value = get.execute(atom); + if (value instanceof Function fn && fn.isThunk()) { + try { + org.enso.interpreter.runtime.EnsoContext ctx = EnsoContext.get(this); + java.lang.Object newValue = invoke.execute(fn, null, State.create(ctx), new Object[0]); + set.execute(atom, newValue); + return newValue; + } catch (AbstractTruffleException ex) { + var rethrow = new SuspendedException(ex); + set.execute(atom, rethrow); + throw ex; + } + } else if (value instanceof SuspendedException suspended) { + throw suspended.ex; + } else { + return value; + } + } + + private static final class SuspendedException implements TruffleObject { + final AbstractTruffleException ex; + + SuspendedException(AbstractTruffleException ex) { + this.ex = ex; + } + } +} diff --git a/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java new file mode 100644 index 000000000000..bc28cd100313 --- /dev/null +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java @@ -0,0 +1,135 @@ +package org.enso.interpreter.test; + +import java.io.ByteArrayOutputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; +import java.util.Map; +import java.util.stream.Collectors; +import org.enso.polyglot.MethodNames; +import org.enso.polyglot.RuntimeOptions; +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.Language; +import org.graalvm.polyglot.Source; +import org.graalvm.polyglot.Value; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import org.junit.Before; +import org.junit.Test; + +public class LazyAtomFieldTest { + private static final ByteArrayOutputStream out = new ByteArrayOutputStream(); + private Context ctx; + + @Before + public void prepareCtx() { + this.ctx = Context.newBuilder() + .allowExperimentalOptions(true) + .allowIO(true) + .allowAllAccess(true) + .logHandler(new ByteArrayOutputStream()) + .out(out) + .option( + RuntimeOptions.LANGUAGE_HOME_OVERRIDE, + Paths.get("../../distribution/component").toFile().getAbsolutePath() + ).build(); + final Map langs = ctx.getEngine().getLanguages(); + assertNotNull("Enso found: " + langs, langs.get("enso")); + out.reset(); + } + + @Test + public void evaluation() throws Exception { + final String code = """ + from Standard.Base import IO + + type Lazy + LazyValue ~x ~y + + say self w = "Hello " + w.to_text + + meaning self = + IO.println "Computing meaning" + v = self.x * self.y + IO.println "Computed meaning" + v + + meanings = + compute_x = + IO.println "Computing x" + v = 6 + IO.println "Computing x done" + v + + compute_y = + IO.println "Computing y" + v = 7 + IO.println "Computing y done" + v + + IO.println "Start" + l = Lazy.LazyValue compute_x compute_y + IO.println "Lazy value ready" + IO.println <| l.say "World!" + IO.println l.meaning + IO.println <| l.say "Again!" + IO.println l.meaning + l.meaning + """; + var meanings = evalCode(code, "meanings"); + assertEquals(42, meanings.asInt()); + + String log = out.toString(StandardCharsets.UTF_8); + var lazyReadyAndThen = log.lines().dropWhile(l -> l.contains("Lazy value ready")).collect(Collectors.toList()); + var computingX = lazyReadyAndThen.stream().filter(l -> l.contains("Computing x done")).count(); + assertEquals(log, 1, computingX); + var computingY = lazyReadyAndThen.stream().filter(l -> l.contains("Computing y done")).count(); + assertEquals(log, 1, computingY); + var hellos = lazyReadyAndThen.stream().filter(l -> l.startsWith("Hello")).count(); + assertEquals(log, 2, hellos); + } + + @Test + public void testInfiniteListGenerator() throws Exception { + final String code = """ + import Standard.Base.IO + + type Lazy + Nil + Cons ~x ~xs + + take self n = if n == 0 then Lazy.Nil else case self of + Lazy.Nil -> Lazy.Nil + Lazy.Cons x xs -> Lazy.Cons x (xs.take n-1) + + sum self acc = case self of + Lazy.Nil -> acc + Lazy.Cons x xs -> @Tail_Call xs.sum acc+x + + generator n = Lazy.Cons n (Lazy.generator n+1) + + both n = + g = Lazy.generator 1 + // IO.println "Generator is computed" + t = g.take n + // IO.println "Generator is taken" + t . sum 0 + """; + + var both = evalCode(code, "both"); + var sum = both.execute(100); + String log = out.toString(StandardCharsets.UTF_8); + assertEquals(log, 5050, sum.asLong()); + } + + private Value evalCode(final String code, final String methodName) throws URISyntaxException { + final var testName = "test.enso"; + final URI testUri = new URI("memory://" + testName); + final Source src = Source.newBuilder("enso", code, testName) + .uri(testUri) + .buildLiteral(); + var module = ctx.eval(src); + return module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, methodName); + } +} diff --git a/test/Tests/src/Runtime/Lazy_Spec.enso b/test/Tests/src/Runtime/Lazy_Spec.enso index ef23b4bd00ed..e024ed534e37 100644 --- a/test/Tests/src/Runtime/Lazy_Spec.enso +++ b/test/Tests/src/Runtime/Lazy_Spec.enso @@ -1,12 +1,16 @@ from Standard.Base import all import Standard.Base.Runtime.Ref.Ref -import Standard.Base.Runtime.Lazy.Lazy import Standard.Base.Errors.Illegal_Argument.Illegal_Argument from Standard.Test import Test, Test_Suite import Standard.Test.Extensions +type Lazy + Value ~get + new ~computation = Lazy.Value computation + new_eager computation = Lazy.Value computation + spec = Test.group "Lazy" <| Test.specify "should compute the result only once" <| ref = Ref.new 0