Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix infinite recursion when calling to_text on an Atom with lazy field #12253

Merged
merged 8 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -862,6 +862,47 @@ public void testSteppingIntoMoreExpressionsOneLine() {
testStepping(src, "foo", new Object[] {0}, steps, expectedLineNumbers);
}

@Test
public void testEvaluateLazyField() {
var fooFunc =
createEnsoMethod(
"""
from Standard.Base.Any import all

type Generator
Value n ~next

natural =
gen n = Generator.Value n (gen n+1)
gen 2

foo x =
two = natural
three = two.next
end = 0
""",
"foo");
try (DebuggerSession session =
debugger.startSession(
(SuspendedEvent event) -> {
switch (event.getSourceSection().getCharacters().toString().strip()) {
case "end = 0" -> {
DebugScope scope = event.getTopStackFrame().getScope();
var twoValue = scope.getDeclaredValue("two");
assertThat(twoValue.isReadable(), is(true));
var nProp = twoValue.getProperty("n");
assertThat("n property should be readable", nProp.asInt(), is(2));
var nextProp = twoValue.getProperty("next");
assertThat("next property is readable", nextProp.isReadable(), is(true));
}
}
event.getSession().suspendNextExecution();
})) {
session.suspendNextExecution();
fooFunc.execute(0);
}
}

private static final class FrameEntry {
private final String scopeName;
private final Map<String, String> values = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.enso.interpreter.test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import java.io.ByteArrayOutputStream;
import java.net.URI;
Expand Down Expand Up @@ -167,6 +168,28 @@ public void fiveAtomObjectFields() throws Exception {
""");
}

@Test
public void toTextOnAtomWithLazyField() throws URISyntaxException {
var res =
evalCode(
"""
from Standard.Base.Any import all

type Generator
Value n ~next

natural =
gen n = Generator.Value n (gen n+1)
gen 2

main _ =
two = natural
two.to_text
""",
"main");
assertTrue(res.isString());
}

private void checkNumHolder(String typeDefinition) throws Exception {
var code =
"polyglot java import java.util.Random as R\n"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.enso.interpreter.test.interop;

import static org.enso.test.utils.ContextUtils.executeInContext;
import static org.enso.test.utils.ContextUtils.unwrapValue;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
Expand Down Expand Up @@ -177,7 +179,7 @@ public void fieldFromPrivateConstructorIsReadable() {

main = My_Type.Cons "a"
""");
ContextUtils.executeInContext(
executeInContext(
ctx,
() -> {
var atom = ContextUtils.unwrapValue(ctx, myTypeAtom);
Expand Down Expand Up @@ -281,7 +283,7 @@ public void fieldIsInvocable() {

main = My_Type.Cons 1 2
""");
ContextUtils.executeInContext(
executeInContext(
ctx,
() -> {
var atom = ContextUtils.unwrapValue(ctx, myTypeAtom);
Expand All @@ -305,7 +307,7 @@ public void fieldIsReadable() {

main = My_Type.Cons 1
""");
ContextUtils.executeInContext(
executeInContext(
ctx,
() -> {
var atom = ContextUtils.unwrapValue(ctx, myTypeAtom);
Expand Down Expand Up @@ -369,4 +371,33 @@ public void typeMembersAreConstructors() {
myType.getMember("Cons_1").canInstantiate(),
is(true));
}

@Test
public void invokeLazyField_DoesNotCauseStackOverflow() {
var atom =
ContextUtils.evalModule(
ctx,
"""
from Standard.Base.Any import all

type Generator
Value n ~next

natural =
gen n = Generator.Value n (gen n+1)
gen 2

main =
natural
""");
executeInContext(
ctx,
() -> {
var atomUnwrapped = unwrapValue(ctx, atom);
var interop = InteropLibrary.getUncached();
var next = interop.invokeMember(atomUnwrapped, "next");
assertThat("Returns next atom", interop.hasMembers(next), is(true));
return null;
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,21 +64,18 @@ private Text consName(AtomConstructor constructor) {

@CompilerDirectives.TruffleBoundary
private Text doComplexAtom(Atom atom) {
var interop = InteropLibrary.getUncached();
var structs = StructsLibrary.getUncached();
Text res = Text.create("(", consName(atom.getConstructor()));
res = Text.create(res, " ");
try {
res = Text.create(res, showObject(structs.getField(atom, 0)));
} catch (UnsupportedMessageException e) {
res = Text.create(res, structs.getField(atom, 0).toString());
}
for (int i = 1; i < atom.getConstructor().getArity(); i++) {
for (int i = 0; i < atom.getConstructor().getArity(); i++) {
res = Text.create(res, " ");
try {
res = Text.create(res, showObject(structs.getField(atom, i)));
} catch (UnsupportedMessageException e) {
res = Text.create(res, structs.getField(atom, i).toString());
if (isFieldSuspended(i, atom)) {
res = Text.create(res, "~" + fieldName(i, atom));
} else {
try {
res = Text.create(res, showObject(structs.getField(atom, i)));
} catch (UnsupportedMessageException e) {
res = Text.create(res, structs.getField(atom, i).toString());
}
}
}
res = Text.create(res, ")");
Expand All @@ -99,4 +96,15 @@ private String showObject(Object child) throws UnsupportedMessageException {
return interop.asString(interop.toDisplayString(child));
}
}

private static boolean isFieldSuspended(int i, Atom atom) {
assert 0 <= i && i < atom.getConstructor().getArity();
var fieldDef = atom.getConstructor().getFields()[i];
return fieldDef.isSuspended();
}

private static String fieldName(int i, Atom atom) {
assert 0 <= i && i < atom.getConstructor().getArity();
return atom.getConstructor().getFields()[i].getName();
}
}
13 changes: 13 additions & 0 deletions test/Base_Tests/src/Runtime/Lazy_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ type Lazy
new ~computation = Lazy.Value computation
new_eager computation = Lazy.Value computation

type Generator
Value n ~next


add_specs suite_builder = suite_builder.group "Lazy" group_builder->
group_builder.specify "should compute the result only once" <|
ref = Ref.new 0
Expand Down Expand Up @@ -96,6 +100,15 @@ add_specs suite_builder = suite_builder.group "Lazy" group_builder->
Test.expect_panic_with matcher=Illegal_Argument <|
Lazy.new_eager (Panic.throw (Illegal_Argument.Error "FOO"))

group_builder.specify "to_text does not evaluate first lazy field" <|
atom = Lazy.new (1+1)
atom.to_text . should_equal "(Lazy.Value ~get)"

group_builder.specify "to_text does not evaluate second lazy field" <|
gen n = Generator.Value n (gen n+1)
two = gen 2
two.to_text . should_equal "(Generator.Value 2 ~next)"

main filter=Nothing =
suite = Test.build suite_builder->
add_specs suite_builder
Expand Down
Loading