| // Copyright 2014 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| package com.google.devtools.build.lib.syntax; | |
| import com.google.common.base.Optional; | |
| import com.google.common.collect.ImmutableList; | |
| import com.google.common.collect.Iterables; | |
| import com.google.devtools.build.lib.events.Location; | |
| import com.google.devtools.build.lib.events.Location.LineAndColumn; | |
| import com.google.devtools.build.lib.profiler.Profiler; | |
| import com.google.devtools.build.lib.profiler.ProfilerTask; | |
| import com.google.devtools.build.lib.syntax.compiler.ByteCodeUtils; | |
| import com.google.devtools.build.lib.syntax.compiler.DebugInfo; | |
| import com.google.devtools.build.lib.syntax.compiler.LoopLabels; | |
| import com.google.devtools.build.lib.syntax.compiler.ReflectionUtils; | |
| import com.google.devtools.build.lib.syntax.compiler.VariableScope; | |
| import com.google.devtools.build.lib.vfs.PathFragment; | |
| import java.io.File; | |
| import java.io.IOException; | |
| import java.io.PrintWriter; | |
| import java.lang.reflect.InvocationTargetException; | |
| import java.lang.reflect.Method; | |
| import java.nio.file.Files; | |
| import java.util.ArrayList; | |
| import java.util.Arrays; | |
| import java.util.Collections; | |
| import java.util.List; | |
| import net.bytebuddy.ByteBuddy; | |
| import net.bytebuddy.asm.ClassVisitorWrapper; | |
| import net.bytebuddy.description.method.MethodDescription; | |
| import net.bytebuddy.description.modifier.MethodManifestation; | |
| import net.bytebuddy.description.modifier.Ownership; | |
| import net.bytebuddy.description.modifier.TypeManifestation; | |
| import net.bytebuddy.description.modifier.Visibility; | |
| import net.bytebuddy.description.type.TypeDescription; | |
| import net.bytebuddy.dynamic.DynamicType.Unloaded; | |
| import net.bytebuddy.dynamic.loading.ClassLoadingStrategy; | |
| import net.bytebuddy.implementation.Implementation; | |
| import net.bytebuddy.implementation.MethodDelegation; | |
| import net.bytebuddy.implementation.bytecode.ByteCodeAppender; | |
| import net.bytebuddy.implementation.bytecode.member.MethodReturn; | |
| import net.bytebuddy.matcher.ElementMatchers; | |
| import org.objectweb.asm.ClassVisitor; | |
| import org.objectweb.asm.ClassWriter; | |
| import org.objectweb.asm.Label; | |
| import org.objectweb.asm.Opcodes; | |
| import org.objectweb.asm.util.Textifier; | |
| import org.objectweb.asm.util.TraceClassVisitor; | |
| /** | |
| * The actual function registered in the environment. This function is defined in the | |
| * parsed code using {@link FunctionDefStatement}. | |
| */ | |
| public class UserDefinedFunction extends BaseFunction { | |
| private final ImmutableList<Statement> statements; | |
| // we close over the globals at the time of definition | |
| private final Environment.Frame definitionGlobals; | |
| private Optional<Method> method; | |
| // TODO(bazel-team) make this configurable once the compiler is stable | |
| public static boolean debugCompiler = false; | |
| public static boolean debugCompilerPrintByteCode = false; | |
| private static File debugFolder; | |
| public static boolean enableCompiler = false; | |
| protected UserDefinedFunction( | |
| Identifier function, | |
| FunctionSignature.WithValues<Object, SkylarkType> signature, | |
| ImmutableList<Statement> statements, | |
| Environment.Frame definitionGlobals) | |
| throws EvalException { | |
| super(function.getName(), signature, function.getLocation()); | |
| this.statements = statements; | |
| this.definitionGlobals = definitionGlobals; | |
| method = enableCompiler ? buildCompiledFunction() : Optional.<Method>absent(); | |
| } | |
| public FunctionSignature.WithValues<Object, SkylarkType> getFunctionSignature() { | |
| return signature; | |
| } | |
| ImmutableList<Statement> getStatements() { | |
| return statements; | |
| } | |
| @Override | |
| public Object call(Object[] arguments, FuncallExpression ast, Environment env) | |
| throws EvalException, InterruptedException { | |
| if (!env.mutability().isMutable()) { | |
| throw new EvalException(getLocation(), "Trying to call in frozen environment"); | |
| } | |
| if (env.getStackTrace().contains(this)) { | |
| throw new EvalException(getLocation(), | |
| String.format("Recursion was detected when calling '%s' from '%s'", | |
| getName(), Iterables.getLast(env.getStackTrace()).getName())); | |
| } | |
| if (enableCompiler && method.isPresent()) { | |
| Object returnValue = callCompiledFunction(arguments, ast, env); | |
| if (returnValue != null) { | |
| return returnValue; | |
| } | |
| } | |
| Profiler.instance().startTask(ProfilerTask.SKYLARK_USER_FN, getName()); | |
| try { | |
| env.enterScope(this, ast, definitionGlobals); | |
| ImmutableList<String> names = signature.getSignature().getNames(); | |
| // Registering the functions's arguments as variables in the local Environment | |
| int i = 0; | |
| for (String name : names) { | |
| env.update(name, arguments[i++]); | |
| } | |
| try { | |
| for (Statement stmt : statements) { | |
| if (stmt instanceof ReturnStatement) { | |
| // Performance optimization. | |
| // Executing the statement would throw an exception, which is slow. | |
| return ((ReturnStatement) stmt).getReturnExpression().eval(env); | |
| } else { | |
| stmt.exec(env); | |
| } | |
| } | |
| } catch (ReturnStatement.ReturnException e) { | |
| return e.getValue(); | |
| } | |
| return Runtime.NONE; | |
| } finally { | |
| Profiler.instance().completeTask(ProfilerTask.SKYLARK_USER_FN); | |
| env.exitScope(); | |
| } | |
| } | |
| private Object callCompiledFunction(Object[] arguments, FuncallExpression ast, Environment env) { | |
| compilerDebug("Calling compiled function " + getLocationPathAndLine() + " " + getName()); | |
| try { | |
| Profiler.instance().startTask(ProfilerTask.SKYLARK_USER_COMPILED_FN, | |
| getLocationPathAndLine() + "#" + getName()); | |
| env.enterScope(this, ast, definitionGlobals); | |
| return method | |
| .get() | |
| .invoke(null, ImmutableList.builder().add(arguments).add(env).build().toArray()); | |
| } catch (IllegalAccessException e) { | |
| // this should never happen | |
| throw new RuntimeException( | |
| "Compiler created code that could not be accessed reflectively.", e); | |
| } catch (InvocationTargetException e) { | |
| compilerDebug("Error running compiled version", e.getCause()); | |
| return null; | |
| } finally { | |
| Profiler.instance().completeTask(ProfilerTask.SKYLARK_USER_COMPILED_FN); | |
| env.exitScope(); | |
| } | |
| } | |
| /** | |
| * Generates a subclass of {@link CompiledFunction} with a static method "call" and static | |
| * methods for getting information from a {@link DebugInfo} instance. | |
| * | |
| * <p>The "call" method contains the compiled version of this function's AST. | |
| */ | |
| private Optional<Method> buildCompiledFunction() throws EvalException { | |
| // replace the / character in the path so we have file system compatible class names | |
| // the java specification mentions that $ should be used in generated code | |
| // see http://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.8 | |
| String path = | |
| location.getPath() != null ? location.getPath().getPathString().replace('/', '$') : ""; | |
| String compiledFunctionClassName = | |
| CompiledFunction.class.getCanonicalName() + path + "$" + getName(); | |
| compilerDebug("Compiling " + getLocationPathAndLine() + " " + getName()); | |
| try { | |
| int publicStatic = Visibility.PUBLIC.getMask() | Ownership.STATIC.getMask(); | |
| TypeDescription.Latent latentCompiledFunctionClass = | |
| new TypeDescription.Latent( | |
| compiledFunctionClassName, | |
| publicStatic | TypeManifestation.FINAL.getMask(), | |
| new TypeDescription.ForLoadedType(CompiledFunction.class), | |
| Collections.<TypeDescription>emptyList()); | |
| MethodDescription getAstNode = | |
| new MethodDescription.Latent( | |
| latentCompiledFunctionClass, | |
| new MethodDescription.Token( | |
| "getAstNode", | |
| publicStatic | MethodManifestation.FINAL.getMask(), | |
| new TypeDescription.ForLoadedType(ASTNode.class), | |
| Arrays.asList(new TypeDescription.ForLoadedType(int.class)))); | |
| MethodDescription getLocation = | |
| new MethodDescription.Latent( | |
| latentCompiledFunctionClass, | |
| new MethodDescription.Token( | |
| "getLocation", | |
| publicStatic | MethodManifestation.FINAL.getMask(), | |
| new TypeDescription.ForLoadedType(Location.class), | |
| Arrays.asList(new TypeDescription.ForLoadedType(int.class)))); | |
| DebugInfo debugInfo = new DebugInfo(getAstNode, getLocation); | |
| FunctionSignature sig = signature.getSignature(); | |
| VariableScope scope = VariableScope.function(sig.getNames()); | |
| Implementation compiledImplementation = compileBody(scope, debugInfo); | |
| List<Class<?>> parameterTypes = sig.getShape().toClasses(); | |
| parameterTypes.add(Environment.class); | |
| Unloaded<CompiledFunction> unloadedImplementation = | |
| new ByteBuddy() | |
| .withClassVisitor(new StackMapFrameClassVisitor(debugCompilerPrintByteCode)) | |
| .subclass(CompiledFunction.class) | |
| .name(compiledFunctionClassName) | |
| .defineMethod( | |
| "call", | |
| Object.class, | |
| parameterTypes, | |
| Visibility.PUBLIC, | |
| Ownership.STATIC, | |
| MethodManifestation.FINAL) | |
| .intercept(compiledImplementation) | |
| .defineMethod(getAstNode) | |
| // TODO(bazel-team) unify the two delegate fields into one, probably needs a custom | |
| // ImplementationDelegate that adds it only once? or just create the static field | |
| // itself with the correct value and create getAstNode & getLocation with a custom | |
| // implementation using it | |
| .intercept( | |
| MethodDelegation.to(debugInfo, DebugInfo.class, "getAstNodeDelegate") | |
| .filter(ElementMatchers.named("getAstNode"))) | |
| .defineMethod(getLocation) | |
| .intercept( | |
| MethodDelegation.to(debugInfo, DebugInfo.class, "getLocationDelegate") | |
| .filter(ElementMatchers.named("getLocation"))) | |
| .make(); | |
| saveByteCode(unloadedImplementation); | |
| Class<? extends CompiledFunction> functionClass = | |
| unloadedImplementation | |
| .load(getClass().getClassLoader(), ClassLoadingStrategy.Default.WRAPPER) | |
| .getLoaded(); | |
| return Optional.of( | |
| ReflectionUtils.getMethod( | |
| functionClass, | |
| "call", | |
| parameterTypes.toArray(new Class<?>[parameterTypes.size()])) | |
| .getLoadedMethod()); | |
| } catch (EvalException e) { | |
| // don't capture EvalExceptions | |
| throw e; | |
| } catch (Throwable e) { | |
| compilerDebug("Error while compiling", e); | |
| // TODO(bazel-team) don't capture all throwables? couldn't compile this, log somewhere? | |
| } | |
| return Optional.absent(); | |
| } | |
| /** | |
| * Saves byte code to a temporary directory prefixed with "skylarkbytecode" in the system | |
| * default temporary directory. | |
| */ | |
| private void saveByteCode(Unloaded<CompiledFunction> unloadedImplementation) { | |
| if (debugCompiler) { | |
| try { | |
| if (debugFolder == null) { | |
| debugFolder = Files.createTempDirectory("skylarkbytecode").toFile(); | |
| } | |
| unloadedImplementation.saveIn(debugFolder); | |
| } catch (IOException e) { | |
| throw new RuntimeException(e); | |
| } | |
| } | |
| } | |
| /** | |
| * Builds a byte code implementation of the AST. | |
| */ | |
| private Implementation compileBody(VariableScope scope, DebugInfo debugInfo) | |
| throws EvalException { | |
| List<ByteCodeAppender> code = new ArrayList<>(statements.size()); | |
| code.add(null); // reserve space for later addition of the local variable initializer | |
| for (Statement statement : statements) { | |
| code.add(statement.compile(scope, LoopLabels.ABSENT, debugInfo)); | |
| } | |
| // add a return None if there are no statements or the last one to ensure the method always | |
| // returns something. This implements the interpreters behavior. | |
| if (statements.isEmpty() | |
| || !(statements.get(statements.size() - 1) instanceof ReturnStatement)) { | |
| code.add(new ByteCodeAppender.Simple(Runtime.GET_NONE, MethodReturn.REFERENCE)); | |
| } | |
| // we now know which variables we used in the method, so assign them "undefined" (i.e. null) | |
| // at the beginning of the method | |
| code.set(0, scope.createLocalVariablesUndefined()); | |
| // TODO(bazel-team) wrap ByteCodeAppender in our own type including a reference to the ASTNode | |
| // it came from and verify the stack and local variables ourselves, because ASM does not help | |
| // with debugging much when its stack map frame calculation fails because of invalid byte code | |
| return new Implementation.Simple(ByteCodeUtils.compoundAppender(code)); | |
| } | |
| /** | |
| * Returns the location (filename:line) of the BaseFunction's definition. | |
| * | |
| * <p>If such a location is not defined, this method returns an empty string. | |
| */ | |
| private String getLocationPathAndLine() { | |
| if (location == null) { | |
| return ""; | |
| } | |
| StringBuilder builder = new StringBuilder(); | |
| PathFragment path = location.getPath(); | |
| if (path != null) { | |
| builder.append(path.getPathString()); | |
| } | |
| LineAndColumn position = location.getStartLineAndColumn(); | |
| if (position != null) { | |
| builder.append(":").append(position.getLine()); | |
| } | |
| return builder.toString(); | |
| } | |
| private void compilerDebug(String message) { | |
| System.err.println(message); | |
| } | |
| private void compilerDebug(String message, Throwable e) { | |
| compilerDebug(message); | |
| e.printStackTrace(); | |
| } | |
| /** | |
| * A simple super class for all compiled function's classes. | |
| */ | |
| protected abstract static class CompiledFunction {} | |
| /** | |
| * A {@link Textifier} for printing the generated byte code that keeps the ASM-internal label | |
| * names in place for easier debugging with IDE debuggers. | |
| */ | |
| private static class DebugTextifier extends Textifier { | |
| DebugTextifier() { | |
| super(Opcodes.ASM5); | |
| } | |
| @Override | |
| protected void appendLabel(Label l) { | |
| buf.append(l.toString()); | |
| } | |
| @Override | |
| protected Textifier createTextifier() { | |
| return new DebugTextifier(); | |
| } | |
| } | |
| /** | |
| * Passes the {@link ClassWriter#COMPUTE_FRAMES} hint to ASM and optionally prints generated | |
| * byte code to System.err. | |
| */ | |
| private static class StackMapFrameClassVisitor implements ClassVisitorWrapper { | |
| private final boolean debug; | |
| private StackMapFrameClassVisitor(boolean debug) { | |
| this.debug = debug; | |
| } | |
| @Override | |
| public int mergeWriter(int hint) { | |
| return hint | ClassWriter.COMPUTE_FRAMES; | |
| } | |
| @Override | |
| public int mergeReader(int hint) { | |
| return hint; | |
| } | |
| @Override | |
| public ClassVisitor wrap(ClassVisitor classVisitor) { | |
| if (debug) { | |
| return new TraceClassVisitor( | |
| classVisitor, new DebugTextifier(), new PrintWriter(System.err, true)); | |
| } else { | |
| return classVisitor; | |
| } | |
| } | |
| } | |
| } |