Skip to content

Commit

Permalink
Suspended atom fields are evaluated only once (#6151)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
JaroslavTulach committed Apr 5, 2023
1 parent 4805193 commit 741b394
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 102 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down
80 changes: 0 additions & 80 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Lazy.enso

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -69,15 +86,22 @@ public void initializeBenchmark(BenchmarkParams params) throws Exception {
this.self = module.invokeMember("get_associated_type");
Function<String,Value> 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);
}
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
Expand All @@ -69,23 +71,30 @@ public Layout(
int[] fieldToStorage,
NodeFactory<? extends UnboxingAtom.FieldGetterNode>[] fieldGetterFactories,
NodeFactory<? extends UnboxingAtom.FieldSetterNode>[] fieldSetterFactories,
NodeFactory<? extends UnboxingAtom.InstantiatorNode> instantiatorFactory) {
NodeFactory<? extends UnboxingAtom.InstantiatorNode> 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++) {
if (fieldSetterFactories[i] != null) {
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) {
Expand All @@ -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");
}
Expand Down Expand Up @@ -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() {
Expand All @@ -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;
}
Expand All @@ -157,15 +170,20 @@ 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) {
return uncachedFieldSetters[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) {
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading

0 comments on commit 741b394

Please sign in to comment.