From aa497f2d347afd4396fb2eaa8e7e24e0a7d8fbe0 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Thu, 30 Mar 2023 17:24:58 +0200 Subject: [PATCH 01/13] Suspended atom fields are evaluated only once --- .../callable/atom/AtomConstructor.java | 2 +- .../callable/atom/unboxing/Layout.java | 69 +++++++++++-- .../callable/atom/unboxing/UnboxingAtom.java | 1 + .../interpreter/test/LazyAtomFieldTest.java | 97 +++++++++++++++++++ 4 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java 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..22de9ad509f0 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 @@ -3,10 +3,18 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.NodeFactory; import com.oracle.truffle.api.nodes.ExplodeLoop; +import com.oracle.truffle.api.nodes.Node; import org.enso.interpreter.dsl.atom.LayoutSpec; +import org.enso.interpreter.node.callable.InvokeCallableNode; +import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode; 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.argument.CallArgumentInfo; +import org.enso.interpreter.runtime.callable.atom.Atom; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; +import org.enso.interpreter.runtime.callable.function.Function; +import org.enso.interpreter.runtime.state.State; /** * This class mediates the use of {@link UnboxingAtom} instances. It is responsible for describing @@ -54,6 +62,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 +78,15 @@ 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 +94,13 @@ 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] = new SuspendedReadCheckNode(this.uncachedFieldGetters[i], this.uncachedFieldSetters[i]); + } + } } public static boolean isAritySupported(int arity) { @@ -98,7 +113,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 +152,9 @@ 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 +165,9 @@ 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()) { + getters[i] = new SuspendedReadCheckNode(getters[i], buildSetter(i)); + } } return getters; } @@ -157,7 +177,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 = new SuspendedReadCheckNode(node, buildSetter(index)); + } + return node; } public UnboxingAtom.FieldSetterNode getUncachedFieldSetter(int index) { @@ -233,7 +257,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(); @@ -269,4 +293,31 @@ long computeFlags(Object[] arguments) { return flags; } } + + private static class SuspendedReadCheckNode extends UnboxingAtom.FieldGetterNode { + private @Node.Child + UnboxingAtom.FieldSetterNode set; + private @Node.Child + UnboxingAtom.FieldGetterNode get; + private @Node.Child + InvokeFunctionNode invoke = InvokeFunctionNode.build(new CallArgumentInfo[0], InvokeCallableNode.DefaultsExecutionMode.EXECUTE, InvokeCallableNode.ArgumentsExecutionMode.EXECUTE); + + private SuspendedReadCheckNode(UnboxingAtom.FieldGetterNode get, UnboxingAtom.FieldSetterNode set) { + this.get = get; + this.set = set; + } + + @Override + public Object execute(Atom atom) { + var value = get.execute(atom); + if (value instanceof Function fn && fn.isThunk()) { + var ctx = EnsoContext.get(this); + var newValue = invoke.execute(fn, null, State.create(ctx), new Object[0]); + set.execute(atom, newValue); + return newValue; + } else { + return value; + } + } + } } diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java index 53c51366d3e6..b03421f436bd 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java @@ -5,6 +5,7 @@ import com.oracle.truffle.api.library.ExportMessage; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.Node; +import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; import org.enso.interpreter.runtime.callable.atom.Atom; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; import org.enso.interpreter.runtime.callable.atom.StructsLibrary; 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..65b61b4751e3 --- /dev/null +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java @@ -0,0 +1,97 @@ +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.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 + + meaning_twice = + 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 "Světe!" + IO.println l.meaning + IO.println <| l.say "Again!" + IO.println l.meaning + l.meaning + """; + var meaning_twice = evalCode(code, "meaning_twice"); + assertEquals(42, meaning_twice.asInt()); + + String log = out.toString(StandardCharsets.UTF_8); + var list = log.lines().filter(l -> l.contains("Computing x done")).collect(Collectors.toList()); + assertEquals(log, 1, list.size()); + } + + 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); + var powers = module.invokeMember("eval_expression", methodName); + return powers; + } +} From 3ab8de527f64f6a81fa70dbcf5d2fb740d4f70cd Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Fri, 31 Mar 2023 04:16:38 +0200 Subject: [PATCH 02/13] More detailed analysis of the log --- .../interpreter/test/LazyAtomFieldTest.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) 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 index 65b61b4751e3..7298991712b5 100644 --- a/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java @@ -54,7 +54,7 @@ public void evaluation() throws Exception { IO.println "Computed meaning" v - meaning_twice = + meanings = compute_x = IO.println "Computing x" v = 6 @@ -70,18 +70,23 @@ public void evaluation() throws Exception { IO.println "Start" l = Lazy.LazyValue compute_x compute_y IO.println "Lazy value ready" - IO.println <| l.say "Světe!" + IO.println <| l.say "World!" IO.println l.meaning IO.println <| l.say "Again!" IO.println l.meaning l.meaning """; - var meaning_twice = evalCode(code, "meaning_twice"); - assertEquals(42, meaning_twice.asInt()); + var meanings = evalCode(code, "meanings"); + assertEquals(42, meanings.asInt()); String log = out.toString(StandardCharsets.UTF_8); - var list = log.lines().filter(l -> l.contains("Computing x done")).collect(Collectors.toList()); - assertEquals(log, 1, list.size()); + 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); } private Value evalCode(final String code, final String methodName) throws URISyntaxException { From 1fc191f26639db0936077e483725582b260d10e4 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Tue, 4 Apr 2023 13:37:51 +0200 Subject: [PATCH 03/13] Change note: One can define lazy atom fields --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d875b80a8fe..dff36a3de14c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -668,6 +668,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 @@ -771,6 +772,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) From 4ad5130dab285a7a9b5ce91d8e548d7847df7269 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Tue, 4 Apr 2023 13:48:30 +0200 Subject: [PATCH 04/13] Using MethodNames constant --- .../java/org/enso/interpreter/test/LazyAtomFieldTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 7298991712b5..a2e7ba186458 100644 --- a/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java @@ -7,6 +7,7 @@ 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; @@ -96,7 +97,6 @@ private Value evalCode(final String code, final String methodName) throws URISyn .uri(testUri) .buildLiteral(); var module = ctx.eval(src); - var powers = module.invokeMember("eval_expression", methodName); - return powers; + return module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, methodName); } } From cb89ab6cf357c3d10400902d4f58a94f5c4026bc Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Tue, 4 Apr 2023 14:18:33 +0200 Subject: [PATCH 05/13] Suspended field getter --- .../callable/atom/unboxing/Layout.java | 42 +++------------- .../unboxing/SuspendedFieldGetterNode.java | 48 +++++++++++++++++++ .../callable/atom/unboxing/UnboxingAtom.java | 1 - 3 files changed, 55 insertions(+), 36 deletions(-) create mode 100644 engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java 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 22de9ad509f0..1a56d0a3ef8c 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 @@ -79,8 +79,7 @@ public Layout( NodeFactory[] fieldGetterFactories, NodeFactory[] fieldSetterFactories, NodeFactory instantiatorFactory, - ArgumentDefinition[] args - ) { + ArgumentDefinition[] args) { this.args = args; this.inputFlags = inputFlags; this.fieldToStorage = fieldToStorage; @@ -98,7 +97,9 @@ public Layout( this.uncachedFieldGetters[i] = fieldGetterFactories[i].getUncachedInstance(); assert this.uncachedFieldGetters[i] != null; if (args[i].isSuspended()) { - this.uncachedFieldGetters[i] = new SuspendedReadCheckNode(this.uncachedFieldGetters[i], this.uncachedFieldSetters[i]); + this.uncachedFieldGetters[i] = + SuspendedFieldGetterNode.build( + this.uncachedFieldGetters[i], this.uncachedFieldSetters[i]); } } } @@ -152,9 +153,7 @@ public static Layout create(int arity, long typeFlags, ArgumentDefinition[] args var instantiatorFactory = LayoutFactory.getInstantiatorNodeFactory(numUnboxed, numBoxed); return new Layout( - typeFlags, fieldToStorage, getterFactories, - setterFactories, instantiatorFactory, args - ); + typeFlags, fieldToStorage, getterFactories, setterFactories, instantiatorFactory, args); } public UnboxingAtom.FieldGetterNode[] getUncachedFieldGetters() { @@ -166,7 +165,7 @@ public UnboxingAtom.FieldGetterNode[] buildGetters() { for (int i = 0; i < fieldGetterFactories.length; i++) { getters[i] = fieldGetterFactories[i].createNode(); if (args[i].isSuspended()) { - getters[i] = new SuspendedReadCheckNode(getters[i], buildSetter(i)); + getters[i] = SuspendedFieldGetterNode.build(getters[i], buildSetter(i)); } } return getters; @@ -179,7 +178,7 @@ public UnboxingAtom.FieldGetterNode getUncachedFieldGetter(int index) { public UnboxingAtom.FieldGetterNode buildGetter(int index) { var node = fieldGetterFactories[index].createNode(); if (args[index].isSuspended()) { - node = new SuspendedReadCheckNode(node, buildSetter(index)); + node = SuspendedFieldGetterNode.build(node, buildSetter(index)); } return node; } @@ -293,31 +292,4 @@ long computeFlags(Object[] arguments) { return flags; } } - - private static class SuspendedReadCheckNode extends UnboxingAtom.FieldGetterNode { - private @Node.Child - UnboxingAtom.FieldSetterNode set; - private @Node.Child - UnboxingAtom.FieldGetterNode get; - private @Node.Child - InvokeFunctionNode invoke = InvokeFunctionNode.build(new CallArgumentInfo[0], InvokeCallableNode.DefaultsExecutionMode.EXECUTE, InvokeCallableNode.ArgumentsExecutionMode.EXECUTE); - - private SuspendedReadCheckNode(UnboxingAtom.FieldGetterNode get, UnboxingAtom.FieldSetterNode set) { - this.get = get; - this.set = set; - } - - @Override - public Object execute(Atom atom) { - var value = get.execute(atom); - if (value instanceof Function fn && fn.isThunk()) { - var ctx = EnsoContext.get(this); - var newValue = invoke.execute(fn, null, State.create(ctx), new Object[0]); - set.execute(atom, newValue); - return newValue; - } else { - return value; - } - } - } } 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..a08e1ca4be7a --- /dev/null +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java @@ -0,0 +1,48 @@ +package org.enso.interpreter.runtime.callable.atom.unboxing; + +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()) { + 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; + } else { + return value; + } + } + +} diff --git a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java index b03421f436bd..53c51366d3e6 100644 --- a/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java +++ b/engine/runtime/src/main/java/org/enso/interpreter/runtime/callable/atom/unboxing/UnboxingAtom.java @@ -5,7 +5,6 @@ import com.oracle.truffle.api.library.ExportMessage; import com.oracle.truffle.api.nodes.ExplodeLoop; import com.oracle.truffle.api.nodes.Node; -import org.enso.interpreter.runtime.callable.argument.ArgumentDefinition; import org.enso.interpreter.runtime.callable.atom.Atom; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; import org.enso.interpreter.runtime.callable.atom.StructsLibrary; From d76cb6e8464f92053c3f36b300bc714e8a85cf67 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Tue, 4 Apr 2023 16:42:52 +0200 Subject: [PATCH 06/13] Testing infinite list generator --- .../callable/atom/unboxing/Layout.java | 13 +++----- .../interpreter/test/LazyAtomFieldTest.java | 33 +++++++++++++++++++ 2 files changed, 37 insertions(+), 9 deletions(-) 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 1a56d0a3ef8c..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 @@ -3,18 +3,11 @@ import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.dsl.NodeFactory; import com.oracle.truffle.api.nodes.ExplodeLoop; -import com.oracle.truffle.api.nodes.Node; import org.enso.interpreter.dsl.atom.LayoutSpec; -import org.enso.interpreter.node.callable.InvokeCallableNode; -import org.enso.interpreter.node.callable.dispatch.InvokeFunctionNode; 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.argument.CallArgumentInfo; -import org.enso.interpreter.runtime.callable.atom.Atom; import org.enso.interpreter.runtime.callable.atom.AtomConstructor; -import org.enso.interpreter.runtime.callable.function.Function; -import org.enso.interpreter.runtime.state.State; /** * This class mediates the use of {@link UnboxingAtom} instances. It is responsible for describing @@ -165,7 +158,8 @@ public UnboxingAtom.FieldGetterNode[] buildGetters() { for (int i = 0; i < fieldGetterFactories.length; i++) { getters[i] = fieldGetterFactories[i].createNode(); if (args[i].isSuspended()) { - getters[i] = SuspendedFieldGetterNode.build(getters[i], buildSetter(i)); + var setterOrNull = buildSetter(i); + getters[i] = SuspendedFieldGetterNode.build(getters[i], setterOrNull); } } return getters; @@ -188,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) { 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 index a2e7ba186458..bc28cd100313 100644 --- a/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java +++ b/engine/runtime/src/test/java/org/enso/interpreter/test/LazyAtomFieldTest.java @@ -90,6 +90,39 @@ public void evaluation() throws Exception { 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); From 160068a682165ee7794dca0e6f5556e09cc73a22 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 08:25:42 +0200 Subject: [PATCH 07/13] Testing lazy atom fields by the Lazy_Spec --- test/Tests/src/Runtime/Lazy_Spec.enso | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From cad4e8e3e71b1379e3077a220bbe1d112ca718f7 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 09:08:03 +0200 Subject: [PATCH 08/13] Keep and rethrow suspended exceptions --- .../unboxing/SuspendedFieldGetterNode.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) 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 index a08e1ca4be7a..6aa33021da6e 100644 --- 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 @@ -1,5 +1,7 @@ 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; @@ -9,7 +11,8 @@ 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 +/** + * 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. * */ @@ -20,7 +23,7 @@ final class SuspendedFieldGetterNode extends UnboxingAtom.FieldGetterNode { private UnboxingAtom.FieldGetterNode get; @Node.Child private InvokeFunctionNode invoke = InvokeFunctionNode.build( - new CallArgumentInfo[0], InvokeCallableNode.DefaultsExecutionMode.EXECUTE, InvokeCallableNode.ArgumentsExecutionMode.EXECUTE + new CallArgumentInfo[0], InvokeCallableNode.DefaultsExecutionMode.EXECUTE, InvokeCallableNode.ArgumentsExecutionMode.EXECUTE ); private SuspendedFieldGetterNode(UnboxingAtom.FieldGetterNode get, UnboxingAtom.FieldSetterNode set) { @@ -36,13 +39,28 @@ static UnboxingAtom.FieldGetterNode build(UnboxingAtom.FieldGetterNode get, Unbo public Object execute(Atom atom) { java.lang.Object value = get.execute(atom); if (value instanceof Function fn && fn.isThunk()) { - 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; + 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; + } + } } From 39819fa7e2e77bf306ca9d4127f6cef8ae9f792a Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 09:13:26 +0200 Subject: [PATCH 09/13] Replacing Lazy.Lazy with ~lazy atom field --- .../Base/0.0.0-dev/src/Runtime/Lazy.enso | 75 ------------------- .../src/Internal/SQL_Type_Reference.enso | 7 +- 2 files changed, 3 insertions(+), 79 deletions(-) delete mode 100644 distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso 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 388eb0984d0d..000000000000 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso +++ /dev/null @@ -1,75 +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 - -## Holds a value that is computed on first access. -type Lazy - ## PRIVATE - Lazy (cached_ref : Ref) (builder : Nothing -> Any) - - ## PRIVATE - Eager (value : Any) - - ## 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 - - ## 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..033511f43281 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 ## Refers to an SQL type that is overridden by the dialect's type system. Overridden (value : SQL_Type) @@ -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. From bcec688cb503a1d275b6006793fa788c6d81ebc6 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 09:53:46 +0200 Subject: [PATCH 10/13] Comparing standard List with lazy list implementation --- .../benchmarks/semantic/ListBenchmarks.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) 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); From 03262ce103d9771b73b37b2147e0314a5753965e Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 10:37:16 +0200 Subject: [PATCH 11/13] Make Table_Tests pass --- .../Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 033511f43281..b4bd6d693ff5 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 @@ -24,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 From 473911dd0a8f46e42f6b59f66259b1cd777d3a34 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 15:29:16 +0200 Subject: [PATCH 12/13] Include SQL_Type in the signature --- .../Database/0.0.0-dev/src/Internal/SQL_Type_Reference.enso | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 b4bd6d693ff5..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 @@ -13,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 + 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) From 3619f8a88b0063239dddce5896b724d471897043 Mon Sep 17 00:00:00 2001 From: Jaroslav Tulach Date: Wed, 5 Apr 2023 15:29:31 +0200 Subject: [PATCH 13/13] Removing empty line --- .../runtime/callable/atom/unboxing/SuspendedFieldGetterNode.java | 1 - 1 file changed, 1 deletion(-) 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 index 6aa33021da6e..5760f8515892 100644 --- 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 @@ -14,7 +14,6 @@ /** * 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