package com.oracle.truffle.api.instrumentation.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.oracle.truffle.api.CallTarget;
import com.oracle.truffle.api.CompilerDirectives;
import com.oracle.truffle.api.CompilerDirectives.CompilationFinal;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import com.oracle.truffle.api.Truffle;
import com.oracle.truffle.api.TruffleLanguage;
import com.oracle.truffle.api.TruffleLanguage.Env;
import com.oracle.truffle.api.frame.Frame;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.frame.VirtualFrame;
import com.oracle.truffle.api.instrumentation.EventContext;
import com.oracle.truffle.api.instrumentation.ExecutionEventListener;
import com.oracle.truffle.api.instrumentation.GenerateWrapper;
import com.oracle.truffle.api.instrumentation.InstrumentableNode;
import com.oracle.truffle.api.instrumentation.ProbeNode;
import com.oracle.truffle.api.instrumentation.ProvidedTags;
import com.oracle.truffle.api.instrumentation.SourceSectionFilter;
import com.oracle.truffle.api.instrumentation.StandardTags;
import com.oracle.truffle.api.instrumentation.Tag;
import com.oracle.truffle.api.instrumentation.TruffleInstrument;
import com.oracle.truffle.api.interop.InteropException;
import com.oracle.truffle.api.interop.InteropLibrary;
import com.oracle.truffle.api.interop.InvalidArrayIndexException;
import com.oracle.truffle.api.interop.NodeLibrary;
import com.oracle.truffle.api.interop.TruffleObject;
import com.oracle.truffle.api.interop.UnknownIdentifierException;
import com.oracle.truffle.api.interop.UnsupportedMessageException;
import com.oracle.truffle.api.library.ExportLibrary;
import com.oracle.truffle.api.library.ExportMessage;
import com.oracle.truffle.api.nodes.Node;
import com.oracle.truffle.api.nodes.RootNode;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.api.source.SourceSection;
import com.oracle.truffle.api.test.polyglot.ProxyLanguage;
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;
public class VariablesScopeTest extends AbstractInstrumentationTest {
private static final InteropLibrary INTEROP = InteropLibrary.getFactory().getUncached();
@Test
public void testILScope() throws Throwable {
assureEnabled(engine.getInstruments().get("testVariablesScopeInstrument"));
TestScopeInstrument.INSTANCE.setTester(new ILScopeTester());
run("ROOT(DEFINE(\ntestFunction,ROOT(\nVARIABLE(a, 10),\nVARIABLE(b, 20),\nSTATEMENT)),\nCALL(testFunction))");
TestScopeInstrument.INSTANCE.checkForFailure();
}
@Test
public void testDefaultScope() throws Throwable {
assureEnabled(engine.getInstruments().get("testVariablesScopeInstrument"));
createDefaultScopeLanguage(context);
DefaultScopeTester scopeTester = new DefaultScopeTester();
TestScopeInstrument.INSTANCE.setTester(scopeTester);
org.graalvm.polyglot.Source source = org.graalvm.polyglot.Source.newBuilder(ProxyLanguage.ID, "test", "file").build();
Value program = context.parse(source);
assertEquals(9, program.execute(4, 5).asInt());
TestScopeInstrument.INSTANCE.checkForFailure();
assertEquals("1Enter2Enter2Exit1Exit", scopeTester.getVisitedLocations());
}
@TruffleInstrument.Registration(id = "testVariablesScopeInstrument", services = Object.class)
public static class TestScopeInstrument extends TruffleInstrument {
static TestScopeInstrument INSTANCE;
private Tester tester;
private boolean scopeTested;
private Throwable failure;
@Override
protected void onCreate(TruffleInstrument.Env env) {
INSTANCE = this;
env.getInstrumenter().attachExecutionEventListener(SourceSectionFilter.newBuilder().tagIs(StandardTags.StatementTag.class).build(), new ExecutionEventListener() {
@Override
@TruffleBoundary
public void onEnter(EventContext context, VirtualFrame frame) {
scopeTested = true;
try {
tester.doTestScope(env, context.getInstrumentedNode(), frame, true);
} catch (Throwable t) {
failure = t;
}
}
@Override
@TruffleBoundary
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
try {
tester.doTestScope(env, context.getInstrumentedNode(), frame, false);
} catch (Throwable t) {
failure = t;
}
}
@Override
public void onReturnExceptional(EventContext context, VirtualFrame frame, Throwable exception) {
}
});
}
void setTester(Tester tester) {
scopeTested = false;
this.tester = tester;
}
void checkForFailure() throws Throwable {
tester = null;
assertTrue("Scope instrument not triggered", scopeTested);
if (failure != null) {
throw failure;
}
}
interface Tester {
void doTestScope(TruffleInstrument.Env env, Node node, VirtualFrame frame, boolean nodeEnter) throws Exception;
}
}
private static int getKeySize(Object object) {
try {
Object keys = INTEROP.getMembers(object);
return (int) INTEROP.getArraySize(keys);
} catch (UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
private static boolean contains(Object object, String key) {
return INTEROP.isMemberReadable(object, key);
}
private static Object read(Object object, String key) {
try {
return INTEROP.readMember(object, key);
} catch (UnknownIdentifierException | UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
private static boolean isNull(Object object) {
return INTEROP.isNull(object);
}
private static class ILScopeTester implements TestScopeInstrument.Tester {
public void doTestScope(TruffleInstrument.Env env, Node node, VirtualFrame frame, boolean nodeEnter) throws Exception {
assertTrue(NodeLibrary.getUncached().hasScope(node, null));
assertTrue(NodeLibrary.getUncached().hasScope(node, frame));
Object lexicalScope = NodeLibrary.getUncached().getScope(node, null, true);
Object dynamicScope = NodeLibrary.getUncached().getScope(node, frame, true);
assertFalse(INTEROP.hasScopeParent(lexicalScope));
assertFalse(INTEROP.hasScopeParent(dynamicScope));
String scopeName = INTEROP.asString(INTEROP.toDisplayString(lexicalScope));
int line = node.getSourceSection().getStartLine();
if (line == 1 || line == 6) {
assertEquals("Line = " + line + ", function name: ", "", scopeName);
} else {
assertEquals("Line = " + line + ", function name: ", "testFunction", scopeName);
TruffleObject vars = (TruffleObject) lexicalScope;
final int numVars = Math.max(line - 3, 0);
int varSize = getKeySize(vars);
assertEquals("Line = " + line + ", num vars:", numVars, varSize);
if (numVars >= 1) {
assertTrue("Var a: ", contains(vars, "a"));
assertTrue(isNull(read(vars, "a")));
}
if (numVars >= 2) {
assertTrue("Var b: ", contains(vars, "b"));
assertTrue(isNull(read(vars, "b")));
}
vars = (TruffleObject) dynamicScope;
varSize = getKeySize(vars);
assertEquals("Line = " + line + ", num vars:", numVars, varSize);
if (numVars >= 1) {
assertTrue("Var a: ", contains(vars, "a"));
assertEquals("Var a: ", 10, read(vars, "a"));
}
if (numVars >= 2) {
assertTrue("Var b: ", contains(vars, "b"));
assertEquals("Var b: ", 20, read(vars, "b"));
}
}
if (line == 6) {
doTestTopScope(env);
}
}
private static void doTestTopScope(TruffleInstrument.Env env) throws UnsupportedMessageException, UnknownIdentifierException, InvalidArrayIndexException {
Object scope = env.getScope(env.getLanguages().get(InstrumentationTestLanguage.ID));
assertFalse(INTEROP.hasScopeParent(scope));
String scopeName = INTEROP.asString(INTEROP.toDisplayString(scope));
assertEquals("global", scopeName);
assertFalse(INTEROP.hasSourceLocation(scope));
Object keys = INTEROP.getMembers(scope);
assertNotNull(keys);
Number size = INTEROP.getArraySize(keys);
assertEquals(1, size.intValue());
String functionName = (String) INTEROP.readArrayElement(keys, 0);
assertEquals("testFunction", functionName);
Object function = INTEROP.readMember(scope, functionName);
assertTrue(INTEROP.isExecutable(function));
}
}
private static class DefaultScopeTester implements TestScopeInstrument.Tester {
private final StringBuilder visitedLocations = new StringBuilder();
public void doTestScope(TruffleInstrument.Env env, Node node, VirtualFrame frame, boolean nodeEnter) throws Exception {
assertTrue(NodeLibrary.getUncached().hasScope(node, null));
assertTrue(NodeLibrary.getUncached().hasScope(node, frame));
Object lexicalScope = NodeLibrary.getUncached().getScope(node, null, nodeEnter);
Object dynamicScope = NodeLibrary.getUncached().getScope(node, frame, nodeEnter);
assertFalse(INTEROP.hasScopeParent(lexicalScope));
assertFalse(INTEROP.hasScopeParent(dynamicScope));
String scopeName = INTEROP.asString(INTEROP.toDisplayString(lexicalScope));
int line = node.getSourceSection().getStartLine();
if (line == 1) {
assertEquals("Line = " + line + ", scope name: ", "local", scopeName);
assertEquals("Lexical arguments", 0, getKeySize(lexicalScope));
assertEquals("Dunamic arguments", 2, getKeySize(dynamicScope));
assertTrue("Argument 0: ", contains(dynamicScope, "0"));
assertTrue("Argument 1: ", contains(dynamicScope, "1"));
assertEquals("Argument 0: ", 4, read(dynamicScope, "0"));
assertEquals("Argument 1: ", 5, read(dynamicScope, "1"));
} else {
assertEquals("Line = " + line + ", scope name: ", "local", scopeName);
TruffleObject vars = (TruffleObject) lexicalScope;
int numVars = nodeEnter ? 2 : 3;
int varSize = getKeySize(vars);
assertEquals("Line = " + line + ", num vars:", numVars, varSize);
if (numVars >= 1) {
assertTrue("Var a: ", contains(vars, "a"));
assertTrue(isNull(read(vars, "a")));
}
if (numVars >= 2) {
assertTrue("Var b: ", contains(vars, "b"));
assertTrue(isNull(read(vars, "b")));
}
if (numVars >= 3) {
assertTrue("Var n: ", contains(vars, "n"));
assertTrue(isNull(read(vars, "n")));
}
vars = (TruffleObject) dynamicScope;
numVars = nodeEnter ? 1 : 2;
varSize = getKeySize(vars);
assertEquals("Line = " + line + ", num vars:", numVars, varSize);
if (numVars >= 1) {
assertTrue("Var a: ", contains(vars, "a"));
assertEquals("Var a: ", 10, read(vars, "a"));
}
if (numVars >= 2) {
assertTrue("Var n: ", contains(vars, "n"));
assertEquals("Var n: ", 2, read(vars, "n"));
}
}
if (line == 2) {
doTestTopScope(env);
}
visitedLocations.append(line);
visitedLocations.append(nodeEnter ? "Enter" : "Exit");
}
private static void doTestTopScope(TruffleInstrument.Env env) throws UnsupportedMessageException {
Object scope = env.getScope(env.getLanguages().get(InstrumentationTestLanguage.ID));
assertFalse(INTEROP.hasScopeParent(scope));
String scopeName = INTEROP.asString(INTEROP.toDisplayString(scope));
assertEquals("global", scopeName);
assertFalse(INTEROP.hasSourceLocation(scope));
Object keys = INTEROP.getMembers(scope);
assertNotNull(keys);
Number size = INTEROP.getArraySize(keys);
assertEquals(0, size.intValue());
}
private String getVisitedLocations() {
return visitedLocations.toString();
}
}
@Test
public void testSPIScopeCalls() throws Throwable {
org.graalvm.polyglot.Source source = org.graalvm.polyglot.Source.create("test-custom-variables-scope-language", "test");
assureEnabled(engine.getInstruments().get("testVariablesScopeInstrument"));
TestScopeInstrument.INSTANCE.setTester(new CustomScopeTester());
context.eval(source);
TestScopeInstrument.INSTANCE.checkForFailure();
}
private static void createDefaultScopeLanguage(Context context) {
ProxyLanguage language = new ProxyLanguage() {
@Override
protected CallTarget parse(TruffleLanguage.ParsingRequest request) throws Exception {
return Truffle.getRuntime().createCallTarget(new RootNode(ProxyLanguage.getCurrentLanguage()) {
@Node.Child private DefaultRootBlockNode block = insert(new DefaultRootBlockNode());
@Override
protected boolean isInstrumentable() {
return true;
}
@Override
public Object execute(VirtualFrame frame) {
return block.execute(frame);
}
});
}
};
ProxyLanguage.setDelegate(language);
context.initialize(ProxyLanguage.ID);
}
@GenerateWrapper
static class DefaultRootBlockNode extends Node implements InstrumentableNode {
@Child private DefaultStatementNode statementNode = new DefaultStatementNode();
@CompilationFinal private FrameSlot a;
@CompilationFinal private FrameSlot b;
@Override
public boolean isInstrumentable() {
return true;
}
@Override
public boolean hasTag(Class<? extends Tag> tag) {
return StandardTags.RootTag.class.equals(tag) || StandardTags.StatementTag.class.equals(tag);
}
@Override
public SourceSection getSourceSection() {
return Source.newBuilder(ProxyLanguage.ID, "RootBlock", "file").build().createSection(1);
}
@Override
public WrapperNode createWrapper(ProbeNode probe) {
return new DefaultRootBlockNodeWrapper(this, probe);
}
Object execute(VirtualFrame frame) {
if (a == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
a = frame.getFrameDescriptor().findOrAddFrameSlot("a");
b = frame.getFrameDescriptor().findOrAddFrameSlot("b");
}
frame.setInt(a, 10);
int ret = statementNode.execute(frame);
frame.setBoolean(b, true);
return ret;
}
}
@GenerateWrapper
static class DefaultStatementNode extends Node implements InstrumentableNode {
@CompilationFinal private FrameSlot n;
@Override
public boolean isInstrumentable() {
return true;
}
@Override
public boolean hasTag(Class<? extends Tag> tag) {
return StandardTags.StatementTag.class.equals(tag);
}
@Override
public SourceSection getSourceSection() {
return Source.newBuilder(ProxyLanguage.ID, "\nStatement", "file").build().createSection(2);
}
@Override
public WrapperNode createWrapper(ProbeNode probe) {
return new DefaultStatementNodeWrapper(this, probe);
}
int execute(VirtualFrame frame) {
if (n == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
n = frame.getFrameDescriptor().findOrAddFrameSlot("n");
}
Object[] arguments = frame.getArguments();
frame.setInt(n, arguments.length);
int s = 0;
for (int i = 0; i < arguments.length; i++) {
s += (int) arguments[i];
}
return s;
}
}
@TruffleLanguage.Registration(name = "", id = "test-custom-variables-scope-language")
@ProvidedTags({StandardTags.StatementTag.class, StandardTags.RootTag.class})
public static class CustomScopeLanguage extends TruffleLanguage<Env> {
@Override
protected Env createContext(Env env) {
return env;
}
@Override
protected CallTarget parse(ParsingRequest request) throws Exception {
return Truffle.getRuntime().createCallTarget(new CustomRoot(this));
}
@Override
protected Object getScope(Env context) {
return new TopScopeObject();
}
@ExportLibrary(InteropLibrary.class)
static final class TopScopeObject implements TruffleObject {
@ExportMessage
@SuppressWarnings("static-method")
boolean hasLanguage() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
Class<? extends TruffleLanguage<?>> getLanguage() {
return CustomScopeLanguage.class;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean isScope() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasMembers() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
Object getMembers(@SuppressWarnings("unused") boolean includeInternal) {
throw new UnsupportedOperationException();
}
@ExportMessage
@SuppressWarnings("static-method")
Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) {
return "TopCustomScope";
}
}
public static class CustomRoot extends RootNode {
@Child private CustomRootBlockNode scopeNode = new CustomRootBlockNode();
public CustomRoot(TruffleLanguage<?> language) {
super(language);
}
@Override
public SourceSection getSourceSection() {
return scopeNode.getSourceSection();
}
@Override
public Object execute(VirtualFrame frame) {
return scopeNode.execute(frame);
}
}
@GenerateWrapper
@ExportLibrary(NodeLibrary.class)
public static class CustomRootBlockNode extends Node implements InstrumentableNode {
@Child private CustomScopeNode scopeNode = new CustomScopeNode();
public CustomRootBlockNode() {
}
@SuppressWarnings("unused")
public Object execute(VirtualFrame frame) {
return scopeNode.execute(frame);
}
@Override
public SourceSection getSourceSection() {
return scopeNode.getSourceSection();
}
public WrapperNode createWrapper(ProbeNode probe) {
return new CustomRootBlockNodeWrapper(this, probe);
}
public boolean isInstrumentable() {
return true;
}
public boolean hasTag(Class<? extends Tag> tag) {
return StandardTags.RootTag.class.equals(tag);
}
@ExportMessage
public boolean hasScope(@SuppressWarnings("unused") Frame frame) {
return true;
}
@ExportMessage
final Object getScope(Frame frame, @SuppressWarnings("unused") boolean nodeEnter) {
if (frame == null) {
return new TestObject(this, "V1");
} else {
return new TestObject(this, "V1V2V3");
}
}
}
@GenerateWrapper
@ExportLibrary(NodeLibrary.class)
public static class CustomScopeNode extends Node implements InstrumentableNode {
public CustomScopeNode() {
}
@SuppressWarnings("unused")
public Object execute(VirtualFrame frame) {
return 1;
}
@Override
public SourceSection getSourceSection() {
return Source.newBuilder("test-custom-variables-scope-language", "test", "unknown").build().createSection(1);
}
public WrapperNode createWrapper(ProbeNode probe) {
return new CustomScopeNodeWrapper(this, probe);
}
public boolean isInstrumentable() {
return true;
}
public boolean hasTag(Class<? extends Tag> tag) {
return StandardTags.StatementTag.class.equals(tag);
}
@ExportMessage
public boolean hasScope(@SuppressWarnings("unused") Frame frame) {
return true;
}
@ExportMessage
final Object getScope(Frame frame, @SuppressWarnings("unused") boolean nodeEnter) {
if (frame == null) {
return new TestObject(this, "V1");
} else {
return new TestObject(this, "V1V2V3");
}
}
}
}
private static class CustomScopeTester implements TestScopeInstrument.Tester {
@Override
public void doTestScope(TruffleInstrument.Env env, Node node, VirtualFrame frame, boolean nodeEnter) {
assertTrue(NodeLibrary.getUncached().hasScope(node, null));
assertTrue(NodeLibrary.getUncached().hasScope(node, frame));
try {
Object lexicalScope = NodeLibrary.getUncached().getScope(node, null, nodeEnter);
Object dynamicScope = NodeLibrary.getUncached().getScope(node, frame, nodeEnter);
assertTrue(INTEROP.isScope(lexicalScope));
assertTrue(INTEROP.isScope(dynamicScope));
testScopeContent(lexicalScope, node, null);
testScopeContent(dynamicScope, node, frame);
assertTrue(INTEROP.hasScopeParent(lexicalScope));
assertTrue(INTEROP.hasScopeParent(dynamicScope));
lexicalScope = INTEROP.getScopeParent(lexicalScope);
dynamicScope = INTEROP.getScopeParent(dynamicScope);
testScopeContent(lexicalScope, node, null);
testScopeContent(dynamicScope, node, frame);
assertFalse(INTEROP.hasScopeParent(lexicalScope));
assertFalse(INTEROP.hasScopeParent(dynamicScope));
doTestTopScope(env);
} catch (UnsupportedMessageException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
private static void testScopeContent(Object scope, Node node, Frame frame) throws UnsupportedMessageException {
assertEquals("CustomScope.getName", INTEROP.asString(INTEROP.toDisplayString(scope)));
assertTrue(INTEROP.hasSourceLocation(scope));
assertEquals(node.getSourceSection(), INTEROP.getSourceLocation(scope));
try {
if (frame == null) {
assertEquals("V1", InteropLibrary.getUncached().readMember(scope, "value"));
} else {
assertEquals("V1V2V3", InteropLibrary.getUncached().readMember(scope, "value"));
}
} catch (InteropException e) {
throw CompilerDirectives.shouldNotReachHere(e);
}
}
private static void doTestTopScope(TruffleInstrument.Env env) throws UnsupportedMessageException {
Object scope = env.getScope(env.getLanguages().get("test-custom-variables-scope-language"));
assertTrue(INTEROP.isScope(scope));
assertFalse(INTEROP.hasScopeParent(scope));
assertEquals("TopCustomScope", INTEROP.asString(INTEROP.toDisplayString(scope)));
assertFalse(INTEROP.hasSourceLocation(scope));
assertTrue(INTEROP.hasMembers(scope));
}
}
@ExportLibrary(InteropLibrary.class)
static final class TestObject implements TruffleObject {
private static final Object MEMBERS = new Members();
private final Node node;
final String value;
TestObject(Node node, String value) {
this.node = node;
this.value = value;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasLanguage() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
Class<? extends TruffleLanguage<?>> getLanguage() {
return CustomScopeLanguage.class;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean isScope() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasMembers() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
Object getMembers(@SuppressWarnings("unused") boolean includeInternal) {
return MEMBERS;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean isMemberReadable(String member) {
return "value".equals(member);
}
@ExportMessage
Object readMember(String member) throws UnknownIdentifierException {
if ("value".equals(member)) {
return value;
} else {
throw UnknownIdentifierException.create(member);
}
}
@ExportMessage
@SuppressWarnings("static-method")
boolean hasSourceLocation() {
return true;
}
@ExportMessage
@TruffleBoundary
SourceSection getSourceLocation() {
return node.getSourceSection();
}
@ExportMessage
boolean hasScopeParent() {
return findParent() != null;
}
@ExportMessage
Object getScopeParent() throws UnsupportedMessageException {
Node parent = findParent();
if (parent != null) {
return new TestObject(parent, value);
} else {
throw UnsupportedMessageException.create();
}
}
private Node findParent() {
Node parent = node.getParent();
if (parent != null && parent instanceof InstrumentableNode.WrapperNode) {
parent = parent.getParent();
}
if (parent != null && !(parent instanceof RootNode)) {
return parent;
} else {
return null;
}
}
@ExportMessage
@SuppressWarnings("static-method")
Object toDisplayString(@SuppressWarnings("unused") boolean allowSideEffects) {
return "CustomScope.getName";
}
@ExportLibrary(InteropLibrary.class)
static final class Members implements TruffleObject {
@ExportMessage
@SuppressWarnings("static-method")
boolean hasArrayElements() {
return true;
}
@ExportMessage
@SuppressWarnings("static-method")
long getArraySize() {
return 1;
}
@ExportMessage
@SuppressWarnings("static-method")
boolean isArrayElementReadable(long index) {
return index == 0;
}
@ExportMessage
@SuppressWarnings("static-method")
Object readArrayElement(long index) throws InvalidArrayIndexException {
if (index == 0) {
return "value";
}
throw InvalidArrayIndexException.create(index);
}
}
}
}