Pretending WithWarnings isException (#9840)

This commit is contained in:
Jaroslav Tulach 2024-05-06 03:40:44 +02:00 committed by GitHub
parent 2af217c3e6
commit 6902f5e0b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 183 additions and 9 deletions

View File

@ -291,10 +291,14 @@ public class BinaryDispatchTest extends TestBase {
} }
} }
private static void assertContains(String expected, String actual) { static void assertContains(String expected, String actual) {
if (actual.contains(expected)) { assertContains("Expecting", expected, actual);
}
static void assertContains(String msg, String expected, String actual) {
if (actual != null && actual.contains(expected)) {
return; return;
} }
fail("Expecting " + expected + " in " + actual); fail(msg + " " + expected + " in " + actual);
} }
} }

View File

@ -20,6 +20,7 @@ import org.junit.Test;
public class MetaIsATest extends TestBase { public class MetaIsATest extends TestBase {
private static Context ctx; private static Context ctx;
private static Value isACheck; private static Value isACheck;
private static Value warningCheck;
private static ValuesGenerator generator; private static ValuesGenerator generator;
@BeforeClass @BeforeClass
@ -30,9 +31,10 @@ public class MetaIsATest extends TestBase {
Source.newBuilder( Source.newBuilder(
"enso", "enso",
""" """
import Standard.Base.Meta from Standard.Base import Meta, Warning
check x y = Meta.is_a x y check x y = Meta.is_a x y
check_warning x = Warning.has_warnings x
""", """,
"check.enso") "check.enso")
.uri(uri) .uri(uri)
@ -40,6 +42,7 @@ public class MetaIsATest extends TestBase {
var module = ctx.eval(src); var module = ctx.eval(src);
isACheck = module.invokeMember("eval_expression", "check"); isACheck = module.invokeMember("eval_expression", "check");
warningCheck = module.invokeMember("eval_expression", "check_warning");
assertTrue("it is a function", isACheck.canExecute()); assertTrue("it is a function", isACheck.canExecute());
} }
@ -283,9 +286,22 @@ public class MetaIsATest extends TestBase {
Value caseOf, Value v, Value t, StringBuilder f, ValuesGenerator g) { Value caseOf, Value v, Value t, StringBuilder f, ValuesGenerator g) {
var test = caseOf.execute(v); var test = caseOf.execute(v);
if (test.isException()) { if (test.isException()) {
// if a generated value isException (from interop point of view)
// then it is either DataflowError or Warning
var vMeta = v.getMetaObject();
if (g.typeError().equals(vMeta)) {
assertEquals("DataFlowError in", g.typeError(), v.getMetaObject()); assertEquals("DataFlowError in", g.typeError(), v.getMetaObject());
assertEquals("DataFlowError out", g.typeError(), test.getMetaObject()); assertEquals("DataFlowError out", g.typeError(), test.getMetaObject());
// end the test here as DataflowError doesn't represent a value
return; return;
} else {
// check if Warning.has_warnings is true
var wv = warningCheck.execute(v);
var wTest = warningCheck.execute(test);
assertTrue("Warning in", wv.asBoolean());
assertTrue("Warning out", wTest.asBoolean());
// but continue with the rest of the test, as Warning still represents a value
}
} }
assertTrue("Expecting 0 or 1 result: " + test + " for " + v, test.isNumber()); assertTrue("Expecting 0 or 1 result: " + test + " for " + v, test.isNumber());
var testBool = test.asInt() == 1; var testBool = test.asInt() == 1;

View File

@ -1,6 +1,8 @@
package org.enso.interpreter.test; package org.enso.interpreter.test;
import static org.enso.interpreter.test.BinaryDispatchTest.assertContains;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import org.enso.common.LanguageInfo; import org.enso.common.LanguageInfo;
@ -10,6 +12,8 @@ import org.enso.interpreter.runtime.error.Warning;
import org.enso.interpreter.runtime.error.WarningsLibrary; import org.enso.interpreter.runtime.error.WarningsLibrary;
import org.enso.interpreter.runtime.error.WithWarnings; import org.enso.interpreter.runtime.error.WithWarnings;
import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.PolyglotException;
import org.graalvm.polyglot.Value;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.Assert; import org.junit.Assert;
import org.junit.BeforeClass; import org.junit.BeforeClass;
@ -18,16 +22,28 @@ import org.junit.Test;
public class WarningsTest extends TestBase { public class WarningsTest extends TestBase {
private static Context ctx; private static Context ctx;
private static ValuesGenerator generator;
private static Value wrap;
private static EnsoContext ensoContext; private static EnsoContext ensoContext;
@BeforeClass @BeforeClass
public static void initEnsoContext() { public static void initEnsoContext() {
ctx = createDefaultContext(); ctx = createDefaultContext();
generator = ValuesGenerator.create(ctx, ValuesGenerator.Language.ENSO);
ensoContext = ensoContext =
(EnsoContext) (EnsoContext)
ctx.getBindings(LanguageInfo.ID) ctx.getBindings(LanguageInfo.ID)
.invokeMember(MethodNames.TopScope.LEAK_CONTEXT) .invokeMember(MethodNames.TopScope.LEAK_CONTEXT)
.asHostObject(); .asHostObject();
var module =
ctx.eval(
"enso",
"""
from Standard.Base import Warning
wrap msg value = Warning.attach msg value
""");
wrap = module.invokeMember(MethodNames.Module.EVAL_EXPRESSION, "wrap");
} }
@AfterClass @AfterClass
@ -65,4 +81,75 @@ public class WarningsTest extends TestBase {
} }
fail("One shall not be created WithWarnings without any warnings " + without); fail("One shall not be created WithWarnings without any warnings " + without);
} }
@Test
public void warningIsAnException() {
var warning42 = wrap.execute("warn:1", 42);
var warningHi = wrap.execute("warn:2", "Hi");
assertTrue("value is a number", warning42.isNumber());
assertTrue("value is Int", warning42.fitsInInt());
assertEquals(42, warning42.asInt());
assertTrue("value2 is a text", warningHi.isString());
assertTrue("value2 not a number", warning42.isNumber());
assertEquals("Hi", warningHi.asString());
assertTrue("value1 with warning is also an exception", warning42.isException());
assertTrue("value2 with warning is also an exception", warningHi.isException());
try {
warning42.throwException();
fail("Shouldn't reach here");
} catch (PolyglotException ex) {
assertEquals("warn:1", ex.getMessage());
}
var warningMulti = wrap.execute("warn:3", warning42);
assertTrue("multi value is a number", warningMulti.isNumber());
assertTrue("multi value is Int", warningMulti.fitsInInt());
assertEquals(42, warningMulti.asInt());
assertTrue("multi vlaue with warning is also an exception", warningMulti.isException());
try {
warningMulti.throwException();
fail("Shouldn't reach here");
} catch (PolyglotException ex) {
assertContains("warn:1", ex.getMessage());
assertContains("warn:3", ex.getMessage());
}
}
@Test
public void allWarningsAreExceptions() throws Exception {
for (var v : generator.allValues()) {
if (v.isNull() || v.isBoolean()) {
continue;
}
assertWarningsForAType(v);
}
}
private void assertWarningsForAType(Value v) {
var type = v.getMetaObject();
var warning1 = wrap.execute("warn:once", v);
var warning2 = wrap.execute("warn:twice", warning1);
var warningType = warning2.getMetaObject();
assertEquals("Types without and with warnings are the same", type, warningType);
assertTrue("It is an exception. Type: " + type, warning2.isException());
try {
warning2.throwException();
} catch (PolyglotException ex) {
if (ex.getMessage() == null) {
assertEquals(generator.typeError(), type);
assertEquals(generator.typeError(), warningType);
} else {
assertContains("Warning found for " + type, "warn:once", ex.getMessage());
assertContains("Warning found for " + type, "warn:twice", ex.getMessage());
}
}
}
} }

View File

@ -32,6 +32,10 @@ public abstract class InteropMethodCallNode extends Node {
return InteropMethodCallNodeGen.create(); return InteropMethodCallNodeGen.create();
} }
public static InteropMethodCallNode getUncached() {
return InteropMethodCallNodeGen.getUncached();
}
/** /**
* Calls the method with given state and arguments. * Calls the method with given state and arguments.
* *

View File

@ -27,6 +27,7 @@ import org.enso.polyglot.common_utils.Core_Text_Utils;
@ExportLibrary(TypesLibrary.class) @ExportLibrary(TypesLibrary.class)
public final class Text implements EnsoObject { public final class Text implements EnsoObject {
private static final Lock LOCK = new ReentrantLock(); private static final Lock LOCK = new ReentrantLock();
private static final Text EMPTY = new Text("");
private volatile Object contents; private volatile Object contents;
private volatile int length = -1; private volatile int length = -1;
private volatile FcdNormalized fcdNormalized = FcdNormalized.UNKNOWN; private volatile FcdNormalized fcdNormalized = FcdNormalized.UNKNOWN;
@ -98,6 +99,10 @@ public final class Text implements EnsoObject {
return false; return false;
} }
public static Text empty() {
return EMPTY;
}
/** /**
* Wraps a string in an instance of Text. * Wraps a string in an instance of Text.
* *

View File

@ -1,7 +1,10 @@
package org.enso.interpreter.runtime.error; package org.enso.interpreter.runtime.error;
import com.oracle.truffle.api.CompilerDirectives; import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.dsl.Bind;
import com.oracle.truffle.api.dsl.Cached.Shared; import com.oracle.truffle.api.dsl.Cached.Shared;
import com.oracle.truffle.api.interop.ArityException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.TruffleObject; import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.interop.UnsupportedMessageException; import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.CachedLibrary; import com.oracle.truffle.api.library.CachedLibrary;
@ -10,21 +13,43 @@ import com.oracle.truffle.api.library.ExportMessage;
import com.oracle.truffle.api.library.Message; import com.oracle.truffle.api.library.Message;
import com.oracle.truffle.api.library.ReflectionLibrary; import com.oracle.truffle.api.library.ReflectionLibrary;
import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.nodes.Node;
import org.enso.interpreter.node.callable.InteropMethodCallNode;
import org.enso.interpreter.runtime.EnsoContext; import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.UnresolvedSymbol;
import org.enso.interpreter.runtime.data.ArrayRope; import org.enso.interpreter.runtime.data.ArrayRope;
import org.enso.interpreter.runtime.data.EnsoObject; import org.enso.interpreter.runtime.data.EnsoObject;
import org.enso.interpreter.runtime.data.Type; import org.enso.interpreter.runtime.data.Type;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.library.dispatch.TypesLibrary; import org.enso.interpreter.runtime.library.dispatch.TypesLibrary;
import org.enso.interpreter.runtime.state.State;
import org.graalvm.collections.EconomicSet; import org.graalvm.collections.EconomicSet;
import org.graalvm.collections.Equivalence; import org.graalvm.collections.Equivalence;
/**
* Represents a typical Enso <em>value with warnings</em>. As much of care as possible is taken to
* delegate all operations to the underlaying {@code value}. Warnings are considered {@link
* InteropLibrary#isException exceptional values} - e.g. one can check for them in Java polyglot
* code as:
*
* <pre>
* Value value = ...;
* if (value.fitsInLong() && value.isException()) {
* // probably an Integer with a warning
* try {
* warningMulti.throwException();
* } catch (PolyglotException ex) {
* System.out.println("Warnings attached to " + value.asLong() + " are " + ex.getMessage());
* }
* }
* </pre>
*/
@ExportLibrary(TypesLibrary.class) @ExportLibrary(TypesLibrary.class)
@ExportLibrary(WarningsLibrary.class) @ExportLibrary(WarningsLibrary.class)
@ExportLibrary(ReflectionLibrary.class) @ExportLibrary(ReflectionLibrary.class)
@ExportLibrary(value = InteropLibrary.class, delegateTo = "value")
public final class WithWarnings implements EnsoObject { public final class WithWarnings implements EnsoObject {
final Object value;
private final EconomicSet<Warning> warnings; private final EconomicSet<Warning> warnings;
private final Object value;
private final boolean limitReached; private final boolean limitReached;
private final int maxWarnings; private final int maxWarnings;
@ -191,6 +216,29 @@ public final class WithWarnings implements EnsoObject {
} }
} }
@CompilerDirectives.TruffleBoundary
private PanicException asException(Node where) {
var rawWarn = this.getWarnings(where, false, WarningsLibrary.getUncached());
var ctx = EnsoContext.get(where);
var scopeOfAny = ctx.getBuiltins().any().getDefinitionScope();
var toText = UnresolvedSymbol.build("to_text", scopeOfAny);
var node = InteropMethodCallNode.getUncached();
var state = State.create(ctx);
var text = Text.empty();
for (var w : rawWarn) {
try {
var wText = node.execute(toText, state, new Object[] {w});
if (wText instanceof Text t) {
text = text.add(t);
}
} catch (ArityException e) {
throw ctx.raiseAssertionPanic(where, null, e);
}
}
return new PanicException(text, where);
}
@ExportMessage @ExportMessage
Object send(Message message, Object[] args, @CachedLibrary(limit = "3") ReflectionLibrary lib) Object send(Message message, Object[] args, @CachedLibrary(limit = "3") ReflectionLibrary lib)
throws Exception { throws Exception {
@ -250,6 +298,16 @@ public final class WithWarnings implements EnsoObject {
return true; return true;
} }
@ExportMessage
boolean isException() {
return true;
}
@ExportMessage
RuntimeException throwException(@Bind("$node") Node node) throws UnsupportedMessageException {
throw asException(node);
}
public static class WarningEquivalence extends Equivalence { public static class WarningEquivalence extends Equivalence {
@Override @Override