Skip to content

Commit

Permalink
Builtin methods handle some exceptions (#7494)
Browse files Browse the repository at this point in the history
MethodProcessor generates code for builtin method invocation that is wrapped in `try-catch` and handles some predefined subset of `RuntimeException`. So far, only `com.oracle.truffle.dsl.api.UnsupportedSpecializationException`.
  • Loading branch information
Akirathan authored Aug 7, 2023
1 parent 3b3dbc3 commit 52b1189
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package org.enso.interpreter.dsl.test;

import com.oracle.truffle.api.nodes.Node;
import java.util.function.Supplier;
import org.enso.interpreter.dsl.BuiltinMethod;
import org.enso.interpreter.runtime.data.text.Text;

@BuiltinMethod(type = "ThrowBuiltinNode", name = "throw")
public class ThrowBuiltinNode extends Node {
public Object execute(Text type, long exceptionIdx) {
switch (type.toString()) {
case "exception" -> {
Supplier<RuntimeException> exceptionSupplier =
ThrowableCatchTest.exceptionSuppliers.get((int) exceptionIdx);
throw exceptionSupplier.get();
}
case "error" -> {
Supplier<Error> errorSupplier =
ThrowableCatchTest.errorSuppliers.get((int) exceptionIdx);
throw errorSupplier.get();
}
default -> throw new AssertionError("Unknown type: " + type);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package org.enso.interpreter.dsl.test;

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

import com.oracle.truffle.api.dsl.UnsupportedSpecializationException;
import java.io.ByteArrayOutputStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.function.Supplier;
import org.enso.interpreter.EnsoLanguage;
import org.enso.interpreter.runtime.EnsoContext;
import org.enso.interpreter.runtime.callable.function.Function;
import org.enso.interpreter.runtime.data.text.Text;
import org.enso.interpreter.runtime.error.DataflowError;
import org.enso.interpreter.runtime.error.PanicException;
import org.enso.polyglot.LanguageInfo;
import org.enso.polyglot.MethodNames.TopScope;
import org.enso.polyglot.RuntimeOptions;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.io.IOAccess;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import com.oracle.truffle.api.nodes.Node;

/**
* Most of the exceptions thrown by the builtin methods, generated by {@link
* org.enso.interpreter.dsl.MethodProcessor}, should not be caught. Only a specific subset of
* exceptions, that we can handle, should be caught, and converted to either a {@link
* PanicException}, or to {@link DataflowError}.
*
* <p>These tests checks this contract.
*/
public class ThrowableCatchTest {
static List<Supplier<RuntimeException>> exceptionSuppliers =
List.of(
() -> new RuntimeException("First exception"),
() -> new IllegalStateException("Illegal state"),
() -> new CustomException(new PanicException("Panic", null)),
() -> new UnsupportedSpecializationException(null, new Node[] {null}, 42));

private static List<Class<?>> shouldBeHandledExceptionTypes =
List.of(UnsupportedSpecializationException.class);

static List<Supplier<Error>> errorSuppliers =
List.of(
() -> new Error("First error"),
() -> new AssertionError("Assertion error"),
CustomError::new,
ThreadDeath::new);

private Context ctx;
private EnsoContext ensoCtx;

private static class CustomException extends RuntimeException {
CustomException(PanicException panic) {
super(panic);
}
}

private static class CustomError extends Error {}

@Before
public void setup() {
ctx =
Context.newBuilder()
.allowExperimentalOptions(true)
.allowIO(IOAccess.ALL)
.allowAllAccess(true)
.logHandler(new ByteArrayOutputStream())
.option(RuntimeOptions.STRICT_ERRORS, "true")
.option(
RuntimeOptions.LANGUAGE_HOME_OVERRIDE,
Paths.get("../../distribution/component").toFile().getAbsolutePath())
.build();
ensoCtx = ctx.getBindings(LanguageInfo.ID).invokeMember(TopScope.LEAK_CONTEXT).asHostObject();
ctx.initialize(LanguageInfo.ID);
ctx.enter();
}

@After
public void tearDown() {
ctx.leave();
ctx.close();
}

@Test
public void testMostRuntimeExceptionsCanPropagateFromBuiltinMethods() {
var func = ThrowBuiltinMethodGen.makeFunction(EnsoLanguage.get(null));
var funcCallTarget = func.getCallTarget();
var emptyState = ensoCtx.emptyState();
for (long exceptionSupplierIdx = 0;
exceptionSupplierIdx < exceptionSuppliers.size();
exceptionSupplierIdx++) {
Object self = null;
Object[] args =
Function.ArgumentsHelper.buildArguments(
func,
null,
emptyState,
new Object[] {self, Text.create("exception"), exceptionSupplierIdx});
try {
funcCallTarget.call(args);
} catch (Throwable t) {
var expectedException = exceptionSuppliers.get((int) exceptionSupplierIdx).get();
if (shouldExceptionBeHandled(t)) {
expectPanicOrDataflowErrorWithMessage(t, expectedException.getMessage());
} else {
assertSameExceptions(
"Thrown RuntimeException should not be modified in the builtin method",
expectedException,
t);
}
}
}
}

@Test
public void testMostErrorsCanPropagateFromBuiltinMethods() {
var func = ThrowBuiltinMethodGen.makeFunction(EnsoLanguage.get(null));
var funcCallTarget = func.getCallTarget();
var emptyState = ensoCtx.emptyState();
for (long errorSupplierIdx = 0; errorSupplierIdx < errorSuppliers.size(); errorSupplierIdx++) {
Object self = null;
Object[] args =
Function.ArgumentsHelper.buildArguments(
func, null, emptyState, new Object[] {self, Text.create("error"), errorSupplierIdx});
try {
funcCallTarget.call(args);
} catch (Throwable t) {
var expectedError = errorSuppliers.get((int) errorSupplierIdx).get();
if (shouldExceptionBeHandled(t)) {
expectPanicOrDataflowErrorWithMessage(t, expectedError.getMessage());
} else {
assertSameExceptions(
"Thrown RuntimeException should not be modified in the builtin method",
expectedError,
t);
}
}
}
}

private void assertSameExceptions(String msg, Throwable expected, Throwable actual) {
assertEquals(msg, expected.getClass(), actual.getClass());
assertEquals(msg, expected.getMessage(), actual.getMessage());
}

private static boolean shouldExceptionBeHandled(Throwable t) {
return shouldBeHandledExceptionTypes.stream()
.anyMatch(exceptionType -> exceptionType.isInstance(t));
}

private void expectPanicOrDataflowErrorWithMessage(Throwable t, String expectedMsg) {
if (t instanceof PanicException panic) {
assertEquals(expectedMsg, panic.getMessage());
} else if (t instanceof DataflowError dataflowError) {
assertEquals(expectedMsg, dataflowError.getMessage());
} else {
fail("Should throw only PanicException or DataflowError: " + t.getMessage());
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.enso.interpreter.dsl;

import com.google.common.base.Strings;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Writer;
Expand Down Expand Up @@ -121,7 +122,11 @@ private void handleTypeElement(TypeElement element, RoundEnvironment roundEnv, B

private final List<String> necessaryImports =
Arrays.asList(
"com.oracle.truffle.api.CompilerDirectives",
"com.oracle.truffle.api.dsl.UnsupportedSpecializationException",
"com.oracle.truffle.api.frame.VirtualFrame",
"com.oracle.truffle.api.nodes.ControlFlowException",
"com.oracle.truffle.api.nodes.Node",
"com.oracle.truffle.api.nodes.NodeInfo",
"com.oracle.truffle.api.nodes.RootNode",
"com.oracle.truffle.api.nodes.UnexpectedResultException",
Expand All @@ -135,13 +140,23 @@ private void handleTypeElement(TypeElement element, RoundEnvironment roundEnv, B
"org.enso.interpreter.runtime.callable.function.Function",
"org.enso.interpreter.runtime.callable.function.FunctionSchema",
"org.enso.interpreter.runtime.EnsoContext",
"org.enso.interpreter.runtime.builtin.Builtins",
"org.enso.interpreter.runtime.data.ArrayRope",
"org.enso.interpreter.runtime.data.text.Text",
"org.enso.interpreter.runtime.error.DataflowError",
"org.enso.interpreter.runtime.error.PanicException",
"org.enso.interpreter.runtime.error.Warning",
"org.enso.interpreter.runtime.error.WithWarnings",
"org.enso.interpreter.runtime.state.State",
"org.enso.interpreter.runtime.type.TypesGen");

/**
* List of exception types that should be caught from the builtin's execute method.
*/
private static final List<String> handleExceptionTypes = List.of(
"UnsupportedSpecializationException"
);

private void generateCode(MethodDefinition methodDefinition) throws IOException {
JavaFileObject gen =
processingEnv.getFiler().createSourceFile(methodDefinition.getQualifiedName());
Expand Down Expand Up @@ -300,14 +315,15 @@ private void generateCode(MethodDefinition methodDefinition) throws IOException
if (warningsPossible) {
out.println(" if (anyWarnings) {");
out.println(" internals.anyWarningsProfile.enter();");
out.println(" Object result = " + executeCall + ";");
out.println(" Object result;");
out.println(wrapInTryCatch("result = " + executeCall + ";", 6));
out.println(" EnsoContext ctx = EnsoContext.get(bodyNode);");
out.println(" return WithWarnings.appendTo(ctx, result, gatheredWarnings);");
out.println(" } else {");
out.println(" return " + executeCall + ";");
out.println(wrapInTryCatch("return " + executeCall + ";", 6));
out.println(" }");
} else {
out.println(" return " + executeCall + ";");
out.println(wrapInTryCatch("return " + executeCall + ";", 6));
}
out.println(" }");

Expand Down Expand Up @@ -345,6 +361,20 @@ private void generateCode(MethodDefinition methodDefinition) throws IOException
}
}

private String wrapInTryCatch(String statement, int indent) {
var indentStr = Strings.repeat(" ", indent);
var sb = new StringBuilder();
sb.append(indentStr).append("try {").append("\n");
sb.append(indentStr).append(" " + statement).append("\n");
sb.append(indentStr).append("} catch (UnsupportedSpecializationException unsupSpecEx) {").append("\n");
sb.append(indentStr).append(" CompilerDirectives.transferToInterpreterAndInvalidate();").append("\n");
sb.append(indentStr).append(" Builtins builtins = EnsoContext.get(bodyNode).getBuiltins();").append("\n");
sb.append(indentStr).append(" var unimplErr = builtins.error().makeUnimplemented(\"Unsupported specialization: \" + unsupSpecEx.getMessage());").append("\n");
sb.append(indentStr).append(" throw new PanicException(unimplErr, bodyNode);").append("\n");
sb.append(indentStr).append("}").append("\n");
return sb.toString();
}

private List<String> generateMakeFunctionArgs(boolean staticInstance, List<ArgumentDefinition> args) {
List<String> argumentDefs = new ArrayList<>();
int staticPrefix = 0;
Expand Down Expand Up @@ -471,7 +501,7 @@ private void generateCheckedArgumentRead(
out.println(
" " + varName + " = " + castName + "(" + argsArray + "[arg" + arg.getPosition() + "Idx]);");
out.println(" } catch (UnexpectedResultException e) {");
out.println(" com.oracle.truffle.api.CompilerDirectives.transferToInterpreter();");
out.println(" CompilerDirectives.transferToInterpreter();");
out.println(" var builtins = EnsoContext.get(bodyNode).getBuiltins();");
out.println(" var ensoTypeName = org.enso.interpreter.runtime.type.ConstantsGen.getEnsoTypeName(\"" + builtinName + "\");");
out.println(" var error = (ensoTypeName != null)");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,16 @@ public boolean validate(ProcessingEnvironment processingEnvironment) {
return false;
}

if (!executeMethod.getThrownTypes().isEmpty()) {
processingEnvironment
.getMessager()
.printMessage(
Kind.ERROR,
"Builtin methods cannot throw exceptions",
executeMethod);
return false;
}

boolean argsValid = arguments.stream().allMatch(arg -> arg.validate(processingEnvironment));

return argsValid;
Expand Down

0 comments on commit 52b1189

Please sign in to comment.