/* * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.oracle.truffle.api.dsl.test.examples; import static com.oracle.truffle.api.dsl.test.examples.ExampleNode.createArguments; import static com.oracle.truffle.api.dsl.test.examples.ExampleNode.createTarget; import static org.junit.Assert.assertEquals; import java.util.HashMap; import java.util.Map; import org.junit.Test; import com.oracle.truffle.api.Assumption; import com.oracle.truffle.api.CallTarget; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.dsl.Cached; import com.oracle.truffle.api.dsl.ImportStatic; import com.oracle.truffle.api.dsl.Specialization; import com.oracle.truffle.api.dsl.test.examples.RubyCallFactory.RubyDispatchNodeGen; import com.oracle.truffle.api.dsl.test.examples.RubyCallFactory.RubyHeadNodeGen; import com.oracle.truffle.api.dsl.test.examples.RubyCallFactory.RubyLookupNodeGen; import com.oracle.truffle.api.frame.VirtualFrame; import com.oracle.truffle.api.nodes.DirectCallNode; import com.oracle.truffle.api.nodes.IndirectCallNode; import com.oracle.truffle.api.nodes.Node; import com.oracle.truffle.api.utilities.CyclicAssumption; /** * This example illustrates a simplified version of a Ruby function call semantics (RubyHeadNode). * The example usage shows how methods can be redefined in this implementation. */ @SuppressWarnings("unused") public class RubyCall { @Test public void testCall() { RubyHeadNode node = RubyHeadNodeGen.create(createArguments(4)); CallTarget nodeTarget = createTarget(node); final Object firstArgument = "someArgument"; // dummyMethod is just going to return the some argument of the function final Object testMethodName = "getSomeArgument"; // implementation returns first argument InternalMethod aClassTestMethod = new InternalMethod(ExampleNode.createDummyTarget(3)); // implementation returns second argument InternalMethod bClassTestMethod = new InternalMethod(ExampleNode.createDummyTarget(4)); // implementation returns third argument InternalMethod cClassTestMethod = new InternalMethod(ExampleNode.createDummyTarget(5)); // defines hierarchy C extends B extends A RubyClass aClass = new RubyClass("A", null); RubyClass bClass = new RubyClass("B", aClass); RubyClass cClass = new RubyClass("C", bClass); RubyObject aInstance = new RubyObject(aClass); RubyObject bInstance = new RubyObject(bClass); RubyObject cInstance = new RubyObject(cClass); // undefined method call assertEquals(RubyObject.NIL, nodeTarget.call(cInstance, testMethodName, null, new Object[]{firstArgument})); // method defined in a aClass.addMethod(testMethodName, aClassTestMethod); assertEquals(firstArgument, nodeTarget.call(aInstance, testMethodName, null, new Object[]{firstArgument})); assertEquals(firstArgument, nodeTarget.call(bInstance, testMethodName, null, new Object[]{firstArgument})); assertEquals(firstArgument, nodeTarget.call(cInstance, testMethodName, null, new Object[]{firstArgument})); // method redefined in b bClass.addMethod(testMethodName, bClassTestMethod); assertEquals(firstArgument, nodeTarget.call(aInstance, testMethodName, null, new Object[]{firstArgument})); assertEquals(firstArgument, nodeTarget.call(bInstance, testMethodName, null, new Object[]{null, firstArgument})); assertEquals(firstArgument, nodeTarget.call(cInstance, testMethodName, null, new Object[]{null, firstArgument})); // method redefined in c cClass.addMethod(testMethodName, cClassTestMethod); assertEquals(firstArgument, nodeTarget.call(aInstance, testMethodName, null, new Object[]{firstArgument})); assertEquals(firstArgument, nodeTarget.call(bInstance, testMethodName, null, new Object[]{null, firstArgument})); assertEquals(firstArgument, nodeTarget.call(cInstance, testMethodName, null, new Object[]{null, null, firstArgument})); } public static class RubyHeadNode extends ExampleNode { @Child private RubyLookupNode lookup = RubyLookupNodeGen.create(); @Child private RubyDispatchNode dispatch = RubyDispatchNodeGen.create(); @Specialization public Object doCall(VirtualFrame frame, RubyObject receiverObject, Object methodName, Object blockObject, Object... argumentsObjects) { InternalMethod method = lookup.executeLookup(receiverObject, methodName); Object[] packedArguments = new Object[argumentsObjects.length + 3]; packedArguments[0] = method; packedArguments[1] = receiverObject; packedArguments[2] = blockObject; System.arraycopy(argumentsObjects, 0, packedArguments, 3, argumentsObjects.length); return dispatch.executeDispatch(frame, method, packedArguments); } } public abstract static class RubyLookupNode extends Node { public abstract InternalMethod executeLookup(RubyObject receiver, Object method); @Specialization(guards = "receiver.getRubyClass() == cachedClass", assumptions = "cachedClass.getDependentAssumptions()") protected static InternalMethod cachedLookup(RubyObject receiver, Object name, // @Cached("receiver.getRubyClass()") RubyClass cachedClass, // @Cached("genericLookup(receiver, name)") InternalMethod cachedLookup) { return cachedLookup; } @Specialization(replaces = "cachedLookup") protected static InternalMethod genericLookup(RubyObject receiver, Object name) { return receiver.getRubyClass().lookup(name); } } @ImportStatic(InternalMethod.class) public abstract static class RubyDispatchNode extends Node { public abstract Object executeDispatch(VirtualFrame frame, InternalMethod function, Object[] packedArguments); /* * Please note that cachedMethod != METHOD_MISSING is invoked once at specialization * instantiation. It is never executed on the fast path. */ @Specialization(guards = {"method == cachedMethod", "cachedMethod != METHOD_MISSING"}) protected static Object directCall(InternalMethod method, Object[] arguments, // @Cached("method") InternalMethod cachedMethod, // @Cached("create(cachedMethod.getTarget())") DirectCallNode callNode) { return callNode.call(arguments); } /* * The method == METHOD_MISSING can fold if the RubyLookup results just in a single entry * returning the constant METHOD_MISSING. */ @Specialization(guards = "method == METHOD_MISSING") protected static Object methodMissing(InternalMethod method, Object[] arguments) { // a real implementation would do a call to a method named method_missing here return RubyObject.NIL; } @Specialization(replaces = "directCall", guards = "method != METHOD_MISSING") protected static Object indirectCall(InternalMethod method, Object[] arguments, // @Cached("create()") IndirectCallNode callNode) { return callNode.call(method.getTarget(), arguments); } } public static final class RubyObject { public static final RubyObject NIL = new RubyObject(null); private final RubyClass rubyClass; public RubyObject(RubyClass rubyClass) { this.rubyClass = rubyClass; } public RubyClass getRubyClass() { return rubyClass; } @Override public String toString() { return "RubyObject[class=" + rubyClass + "]"; } } public static final class RubyClass /* this would extend RubyModule */ { private final String name; private final RubyClass parent; // this would be a RubyModule private final CyclicAssumption unmodified; private final Map<Object, InternalMethod> methods = new HashMap<>(); private Assumption[] cachedDependentAssumptions; private final int depth; public RubyClass(String name, RubyClass parent) { this.name = name; this.parent = parent; this.unmodified = new CyclicAssumption("unmodified class " + name); // lookup depth for array allocation RubyClass clazz = parent; int currentDepth = 1; while (clazz != null) { currentDepth++; clazz = clazz.parent; } this.depth = currentDepth; } @TruffleBoundary public InternalMethod lookup(Object methodName) { InternalMethod method = methods.get(methodName); if (method == null) { if (parent != null) { return parent.lookup(methodName); } else { return InternalMethod.METHOD_MISSING; } } else { return method; } } @TruffleBoundary public void addMethod(Object methodName, InternalMethod method) { // check for existing method omitted for simplicity this.methods.put(methodName, method); this.unmodified.invalidate(); } /* * Method collects all unmodified assumptions in the class hierarchy. The result is cached * per class to void recreation per call site. */ @TruffleBoundary public Assumption[] getDependentAssumptions() { Assumption[] dependentAssumptions = cachedDependentAssumptions; if (dependentAssumptions != null) { // we can use the cached dependent assumptions only if they are still valid for (Assumption assumption : cachedDependentAssumptions) { if (!assumption.isValid()) { dependentAssumptions = null; break; } } } if (dependentAssumptions == null) { cachedDependentAssumptions = dependentAssumptions = createDependentAssumptions(); } return dependentAssumptions; } @Override public String toString() { return "RubyClass[name=" + name + "]"; } private Assumption[] createDependentAssumptions() { Assumption[] dependentAssumptions; RubyClass clazz = this; dependentAssumptions = new Assumption[depth]; // populate array int index = 0; do { dependentAssumptions[index] = clazz.unmodified.getAssumption(); index++; clazz = clazz.parent; } while (clazz != null); return dependentAssumptions; } } public static final class InternalMethod { public static final InternalMethod METHOD_MISSING = new InternalMethod(null); private final CallTarget target; public InternalMethod(CallTarget target) { this.target = target; } public CallTarget getTarget() { return target; } @Override public String toString() { return "InternalMethod[target=" + getTarget() + "]"; } } }