Browse Source

Script: User tree ToXContent (#70893)

Adds ability to serialize the user tree to an
XContent builder, handling all user tree decorations.

To avoid creating a tight dependency, the `ToXContent`
implementation is kept outside the relevant nodes.

Uses a wrapper around the `XContentBuilder` to change
checked `IOExceptions` into runtime exceptions to conform
to UserTreeVisitor API contract.

Adds new debugger method, `Debugger.phases` which
allows the caller to attach visitors at the three main phases
of Painless compilation.
Stuart Tettemer 4 years ago
parent
commit
9d622e52e4

+ 42 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/Compiler.java

@@ -18,12 +18,15 @@ import org.elasticsearch.painless.phase.DefaultIRTreeToASMBytesPhase;
 import org.elasticsearch.painless.phase.DefaultStaticConstantExtractionPhase;
 import org.elasticsearch.painless.phase.DefaultStringConcatenationOptimizationPhase;
 import org.elasticsearch.painless.phase.DocFieldsPhase;
+import org.elasticsearch.painless.phase.IRTreeVisitor;
 import org.elasticsearch.painless.phase.PainlessSemanticAnalysisPhase;
 import org.elasticsearch.painless.phase.PainlessSemanticHeaderPhase;
 import org.elasticsearch.painless.phase.PainlessUserTreeToIRTreePhase;
+import org.elasticsearch.painless.phase.UserTreeVisitor;
 import org.elasticsearch.painless.spi.Whitelist;
 import org.elasticsearch.painless.symbol.Decorations.IRNodeDecoration;
 import org.elasticsearch.painless.symbol.ScriptScope;
+import org.elasticsearch.painless.symbol.WriteScope;
 import org.objectweb.asm.util.Printer;
 
 import java.lang.reflect.Method;
@@ -248,7 +251,6 @@ final class Compiler {
         ScriptScope scriptScope = new ScriptScope(painlessLookup, settings, scriptClassInfo, scriptName, source, root.getIdentifier() + 1);
         new PainlessSemanticHeaderPhase().visitClass(root, scriptScope);
         new PainlessSemanticAnalysisPhase().visitClass(root, scriptScope);
-        // TODO: Make this phase optional #60156
         new DocFieldsPhase().visitClass(root, scriptScope);
         new PainlessUserTreeToIRTreePhase().visitClass(root, scriptScope);
         ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
@@ -260,4 +262,43 @@ final class Compiler {
 
         return classNode.getBytes();
     }
+
+    /**
+     * Runs the two-pass compiler to generate a Painless script with option visitors for each major phase.
+     */
+    byte[] compile(String name, String source, CompilerSettings settings, Printer debugStream,
+                   UserTreeVisitor<ScriptScope> semanticPhaseVisitor,
+                   UserTreeVisitor<ScriptScope> irPhaseVisitor,
+                   IRTreeVisitor<WriteScope> asmPhaseVisitor) {
+        String scriptName = Location.computeSourceName(name);
+        ScriptClassInfo scriptClassInfo = new ScriptClassInfo(painlessLookup, scriptClass);
+        SClass root = Walker.buildPainlessTree(scriptName, source, settings);
+        ScriptScope scriptScope = new ScriptScope(painlessLookup, settings, scriptClassInfo, scriptName, source, root.getIdentifier() + 1);
+
+        new PainlessSemanticHeaderPhase().visitClass(root, scriptScope);
+        new PainlessSemanticAnalysisPhase().visitClass(root, scriptScope);
+        if (semanticPhaseVisitor != null) {
+            semanticPhaseVisitor.visitClass(root, scriptScope);
+        }
+
+        new DocFieldsPhase().visitClass(root, scriptScope);
+        new PainlessUserTreeToIRTreePhase().visitClass(root, scriptScope);
+        if (irPhaseVisitor != null) {
+            irPhaseVisitor.visitClass(root, scriptScope);
+        }
+
+        ClassNode classNode = (ClassNode)scriptScope.getDecoration(root, IRNodeDecoration.class).getIRNode();
+        new DefaultStringConcatenationOptimizationPhase().visitClass(classNode, null);
+        new DefaultConstantFoldingOptimizationPhase().visitClass(classNode, null);
+        new DefaultStaticConstantExtractionPhase().visitClass(classNode, scriptScope);
+        classNode.setDebugStream(debugStream);
+
+        WriteScope writeScope = WriteScope.newScriptScope();
+        new DefaultIRTreeToASMBytesPhase().visitClass(classNode, writeScope);
+        if (asmPhaseVisitor != null) {
+            asmPhaseVisitor.visitClass(classNode, writeScope);
+        }
+
+        return classNode.getBytes();
+    }
 }

+ 1 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/node/ENewArrayFunctionRef.java

@@ -14,7 +14,7 @@ import org.elasticsearch.painless.phase.UserTreeVisitor;
 import java.util.Objects;
 
 /**
- * Represents a function reference.
+ * Represents a function reference for creating a new array (eg Double[]::new)
  */
 public class ENewArrayFunctionRef extends AExpression {
 

+ 2 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/phase/UserTreeBaseVisitor.java

@@ -62,8 +62,8 @@ public class UserTreeBaseVisitor<Scope> implements UserTreeVisitor<Scope> {
     }
 
     @Override
-    public void visitFunction(SFunction userClassNode, Scope scope) {
-        userClassNode.visitChildren(this, scope);
+    public void visitFunction(SFunction userFunctionNode, Scope scope) {
+        userFunctionNode.visitChildren(this, scope);
     }
 
     @Override

+ 8 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/symbol/Decorator.java

@@ -87,4 +87,12 @@ public class Decorator {
 
         return false;
     }
+
+    public Map<Class<? extends Decoration>, Decoration> getAllDecorations(int identifier) {
+        return decorations.get(identifier);
+    }
+
+    public Set<Class<? extends Condition>> getAllConditions(int identifier) {
+        return conditions.get(identifier);
+    }
 }

+ 1 - 1
modules/lang-painless/src/main/java/org/elasticsearch/painless/symbol/ScriptScope.java

@@ -49,7 +49,7 @@ public class ScriptScope extends Decorator {
         this.compilerSettings = Objects.requireNonNull(compilerSettings);
         this.scriptClassInfo = Objects.requireNonNull(scriptClassInfo);
         this.scriptName = Objects.requireNonNull(scriptName);
-        this.scriptSource = Objects.requireNonNull(scriptName);
+        this.scriptSource = Objects.requireNonNull(scriptSource);
 
         staticConstants.put("$NAME", scriptName);
         staticConstants.put("$SOURCE", scriptSource);

+ 648 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/toxcontent/DecorationToXContent.java

@@ -0,0 +1,648 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.painless.toxcontent;
+
+import org.elasticsearch.painless.FunctionRef;
+import org.elasticsearch.painless.lookup.PainlessCast;
+import org.elasticsearch.painless.lookup.PainlessClassBinding;
+import org.elasticsearch.painless.lookup.PainlessConstructor;
+import org.elasticsearch.painless.lookup.PainlessField;
+import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
+import org.elasticsearch.painless.lookup.PainlessMethod;
+import org.elasticsearch.painless.spi.annotation.CompileTimeOnlyAnnotation;
+import org.elasticsearch.painless.spi.annotation.DeprecatedAnnotation;
+import org.elasticsearch.painless.spi.annotation.InjectConstantAnnotation;
+import org.elasticsearch.painless.spi.annotation.NoImportAnnotation;
+import org.elasticsearch.painless.spi.annotation.NonDeterministicAnnotation;
+import org.elasticsearch.painless.symbol.Decorator.Decoration;
+import org.elasticsearch.painless.symbol.Decorations.TargetType;
+import org.elasticsearch.painless.symbol.Decorations.ValueType;
+import org.elasticsearch.painless.symbol.Decorations.StaticType;
+import org.elasticsearch.painless.symbol.Decorations.PartialCanonicalTypeName;
+import org.elasticsearch.painless.symbol.Decorations.ExpressionPainlessCast;
+import org.elasticsearch.painless.symbol.Decorations.SemanticVariable;
+import org.elasticsearch.painless.symbol.Decorations.IterablePainlessMethod;
+import org.elasticsearch.painless.symbol.Decorations.UnaryType;
+import org.elasticsearch.painless.symbol.Decorations.BinaryType;
+import org.elasticsearch.painless.symbol.Decorations.ShiftType;
+import org.elasticsearch.painless.symbol.Decorations.ComparisonType;
+import org.elasticsearch.painless.symbol.Decorations.CompoundType;
+import org.elasticsearch.painless.symbol.Decorations.UpcastPainlessCast;
+import org.elasticsearch.painless.symbol.Decorations.DowncastPainlessCast;
+import org.elasticsearch.painless.symbol.Decorations.StandardPainlessField;
+import org.elasticsearch.painless.symbol.Decorations.StandardPainlessConstructor;
+import org.elasticsearch.painless.symbol.Decorations.StandardPainlessMethod;
+import org.elasticsearch.painless.symbol.Decorations.GetterPainlessMethod;
+import org.elasticsearch.painless.symbol.Decorations.SetterPainlessMethod;
+import org.elasticsearch.painless.symbol.Decorations.StandardConstant;
+import org.elasticsearch.painless.symbol.Decorations.StandardLocalFunction;
+import org.elasticsearch.painless.symbol.Decorations.StandardPainlessClassBinding;
+import org.elasticsearch.painless.symbol.Decorations.StandardPainlessInstanceBinding;
+import org.elasticsearch.painless.symbol.Decorations.MethodNameDecoration;
+import org.elasticsearch.painless.symbol.Decorations.ReturnType;
+import org.elasticsearch.painless.symbol.Decorations.TypeParameters;
+import org.elasticsearch.painless.symbol.Decorations.ParameterNames;
+import org.elasticsearch.painless.symbol.Decorations.ReferenceDecoration;
+import org.elasticsearch.painless.symbol.Decorations.EncodingDecoration;
+import org.elasticsearch.painless.symbol.Decorations.CapturesDecoration;
+import org.elasticsearch.painless.symbol.Decorations.InstanceType;
+import org.elasticsearch.painless.symbol.Decorations.AccessDepth;
+import org.elasticsearch.painless.symbol.Decorations.IRNodeDecoration;
+import org.elasticsearch.painless.symbol.Decorations.Converter;
+import org.elasticsearch.painless.symbol.FunctionTable;
+import org.elasticsearch.painless.symbol.SemanticScope;
+import org.objectweb.asm.Type;
+
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Serialize user tree decorations from org.elasticsearch.painless.symbol.Decorations
+ */
+public class DecorationToXContent {
+
+    static final class Fields {
+        static final String DECORATION = "decoration";
+        static final String TYPE = "type";
+        static final String CAST = "cast";
+        static final String METHOD = "method";
+    }
+
+    public static void ToXContent(TargetType targetType, XContentBuilderWrapper builder) {
+        start(targetType, builder);
+        builder.field(Fields.TYPE, targetType.getTargetType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(ValueType valueType, XContentBuilderWrapper builder) {
+        start(valueType, builder);
+        builder.field(Fields.TYPE, valueType.getValueType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(StaticType staticType, XContentBuilderWrapper builder) {
+        start(staticType, builder);
+        builder.field(Fields.TYPE, staticType.getStaticType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(PartialCanonicalTypeName partialCanonicalTypeName, XContentBuilderWrapper builder) {
+        start(partialCanonicalTypeName, builder);
+        builder.field(Fields.TYPE, partialCanonicalTypeName.getPartialCanonicalTypeName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(ExpressionPainlessCast expressionPainlessCast, XContentBuilderWrapper builder) {
+        start(expressionPainlessCast, builder);
+        builder.field(Fields.CAST);
+        ToXContent(expressionPainlessCast.getExpressionPainlessCast(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(SemanticVariable semanticVariable, XContentBuilderWrapper builder) {
+        start(semanticVariable, builder);
+        builder.field("variable");
+        ToXContent(semanticVariable.getSemanticVariable(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(IterablePainlessMethod iterablePainlessMethod, XContentBuilderWrapper builder) {
+        start(iterablePainlessMethod, builder);
+        builder.field(Fields.METHOD);
+        ToXContent(iterablePainlessMethod.getIterablePainlessMethod(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(UnaryType unaryType, XContentBuilderWrapper builder) {
+        start(unaryType, builder);
+        builder.field(Fields.TYPE, unaryType.getUnaryType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(BinaryType binaryType, XContentBuilderWrapper builder) {
+        start(binaryType, builder);
+        builder.field(Fields.TYPE, binaryType.getBinaryType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(ShiftType shiftType, XContentBuilderWrapper builder) {
+        start(shiftType, builder);
+        builder.field(Fields.TYPE, shiftType.getShiftType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(ComparisonType comparisonType, XContentBuilderWrapper builder) {
+        start(comparisonType, builder);
+        builder.field(Fields.TYPE, comparisonType.getComparisonType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(CompoundType compoundType, XContentBuilderWrapper builder) {
+        start(compoundType, builder);
+        builder.field(Fields.TYPE, compoundType.getCompoundType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(UpcastPainlessCast upcastPainlessCast, XContentBuilderWrapper builder) {
+        start(upcastPainlessCast, builder);
+        builder.field(Fields.CAST);
+        ToXContent(upcastPainlessCast.getUpcastPainlessCast(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(DowncastPainlessCast downcastPainlessCast, XContentBuilderWrapper builder) {
+        start(downcastPainlessCast, builder);
+        builder.field(Fields.CAST);
+        ToXContent(downcastPainlessCast.getDowncastPainlessCast(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardPainlessField standardPainlessField, XContentBuilderWrapper builder) {
+        start(standardPainlessField, builder);
+        builder.field("field");
+        ToXContent(standardPainlessField.getStandardPainlessField(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardPainlessConstructor standardPainlessConstructor, XContentBuilderWrapper builder) {
+        start(standardPainlessConstructor, builder);
+        builder.field("constructor");
+        ToXContent(standardPainlessConstructor.getStandardPainlessConstructor(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardPainlessMethod standardPainlessMethod, XContentBuilderWrapper builder) {
+        start(standardPainlessMethod, builder);
+        builder.field(Fields.METHOD);
+        ToXContent(standardPainlessMethod.getStandardPainlessMethod(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(GetterPainlessMethod getterPainlessMethod, XContentBuilderWrapper builder) {
+        start(getterPainlessMethod, builder);
+        builder.field(Fields.METHOD);
+        ToXContent(getterPainlessMethod.getGetterPainlessMethod(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(SetterPainlessMethod setterPainlessMethod, XContentBuilderWrapper builder) {
+        start(setterPainlessMethod, builder);
+        builder.field(Fields.METHOD);
+        ToXContent(setterPainlessMethod.getSetterPainlessMethod(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardConstant standardConstant, XContentBuilderWrapper builder) {
+        start(standardConstant, builder);
+        builder.startObject("constant");
+        builder.field(Fields.TYPE, standardConstant.getStandardConstant().getClass().getSimpleName());
+        builder.field("value", standardConstant.getStandardConstant());
+        builder.endObject();
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardLocalFunction standardLocalFunction, XContentBuilderWrapper builder) {
+        start(standardLocalFunction, builder);
+        builder.field("function");
+        ToXContent(standardLocalFunction.getLocalFunction(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardPainlessClassBinding standardPainlessClassBinding, XContentBuilderWrapper builder) {
+        start(standardPainlessClassBinding, builder);
+        builder.field("PainlessClassBinding");
+        ToXContent(standardPainlessClassBinding.getPainlessClassBinding(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(StandardPainlessInstanceBinding standardPainlessInstanceBinding, XContentBuilderWrapper builder) {
+        start(standardPainlessInstanceBinding, builder);
+        builder.field("PainlessInstanceBinding");
+        ToXContent(standardPainlessInstanceBinding.getPainlessInstanceBinding(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(MethodNameDecoration methodNameDecoration, XContentBuilderWrapper builder) {
+        start(methodNameDecoration, builder);
+        builder.field("methodName", methodNameDecoration.getMethodName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(ReturnType returnType, XContentBuilderWrapper builder) {
+        start(returnType, builder);
+        builder.field("returnType", returnType.getReturnType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(TypeParameters typeParameters, XContentBuilderWrapper builder) {
+        start(typeParameters, builder);
+        if (typeParameters.getTypeParameters().isEmpty() == false) {
+            builder.field("typeParameters", classNames(typeParameters.getTypeParameters()));
+        }
+        builder.endObject();
+    }
+
+    public static void ToXContent(ParameterNames parameterNames, XContentBuilderWrapper builder) {
+        start(parameterNames, builder);
+        if (parameterNames.getParameterNames().isEmpty() == false) {
+            builder.field("parameterNames", parameterNames.getParameterNames());
+        }
+        builder.endObject();
+    }
+
+    public static void ToXContent(ReferenceDecoration referenceDecoration, XContentBuilderWrapper builder) {
+        start(referenceDecoration, builder);
+        FunctionRef ref = referenceDecoration.getReference();
+        builder.field("interfaceMethodName", ref.interfaceMethodName);
+
+        builder.field("interfaceMethodType");
+        ToXContent(ref.interfaceMethodType, builder);
+
+        builder.field("delegateClassName", ref.delegateClassName);
+        builder.field("isDelegateInterface", ref.isDelegateInterface);
+        builder.field("isDelegateAugmented", ref.isDelegateAugmented);
+        builder.field("delegateInvokeType", ref.delegateInvokeType);
+        builder.field("delegateMethodName", ref.delegateMethodName);
+
+        builder.field("delegateMethodType");
+        ToXContent(ref.delegateMethodType, builder);
+
+        if (ref.delegateInjections.length > 0) {
+            builder.startArray("delegateInjections");
+            for (Object obj : ref.delegateInjections) {
+                builder.startObject();
+                builder.field("type", obj.getClass().getSimpleName());
+                builder.field("value", obj);
+                builder.endObject();
+            }
+            builder.endArray();
+        }
+
+        builder.field("factoryMethodType");
+        ToXContent(ref.factoryMethodType, builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(EncodingDecoration encodingDecoration, XContentBuilderWrapper builder) {
+        start(encodingDecoration, builder);
+        builder.field("encoding", encodingDecoration.getEncoding());
+        builder.endObject();
+    }
+
+    public static void ToXContent(CapturesDecoration capturesDecoration, XContentBuilderWrapper builder) {
+        start(capturesDecoration, builder);
+        if (capturesDecoration.getCaptures().isEmpty() == false) {
+            builder.startArray("captures");
+            for (SemanticScope.Variable capture : capturesDecoration.getCaptures()) {
+                ToXContent(capture, builder);
+            }
+            builder.endArray();
+        }
+        builder.endObject();
+    }
+
+    public static void ToXContent(InstanceType instanceType, XContentBuilderWrapper builder) {
+        start(instanceType, builder);
+        builder.field("instanceType", instanceType.getInstanceType().getSimpleName());
+        builder.endObject();
+    }
+
+    public static void ToXContent(AccessDepth accessDepth, XContentBuilderWrapper builder) {
+        start(accessDepth, builder);
+        builder.field("depth", accessDepth.getAccessDepth());
+        builder.endObject();
+    }
+
+    public static void ToXContent(IRNodeDecoration irNodeDecoration, XContentBuilderWrapper builder) {
+        start(irNodeDecoration, builder);
+        // TODO(stu): expand this
+        builder.field("irNode", irNodeDecoration.getIRNode().toString());
+        builder.endObject();
+    }
+
+    public static void ToXContent(Converter converter, XContentBuilderWrapper builder) {
+        start(converter, builder);
+        builder.field("converter");
+        ToXContent(converter.getConverter(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(Decoration decoration, XContentBuilderWrapper builder) {
+        if  (decoration instanceof TargetType) {
+            ToXContent((TargetType) decoration, builder);
+        } else if (decoration instanceof ValueType) {
+            ToXContent((ValueType) decoration, builder);
+        } else if (decoration instanceof StaticType) {
+            ToXContent((StaticType) decoration, builder);
+        } else if (decoration instanceof PartialCanonicalTypeName) {
+            ToXContent((PartialCanonicalTypeName) decoration, builder);
+        } else if (decoration instanceof ExpressionPainlessCast) {
+            ToXContent((ExpressionPainlessCast) decoration, builder);
+        } else if (decoration instanceof SemanticVariable) {
+            ToXContent((SemanticVariable) decoration, builder);
+        } else if (decoration instanceof IterablePainlessMethod) {
+            ToXContent((IterablePainlessMethod) decoration, builder);
+        } else if (decoration instanceof UnaryType) {
+            ToXContent((UnaryType) decoration, builder);
+        } else if (decoration instanceof BinaryType) {
+            ToXContent((BinaryType) decoration, builder);
+        } else if (decoration instanceof ShiftType) {
+            ToXContent((ShiftType) decoration, builder);
+        } else if (decoration instanceof ComparisonType) {
+            ToXContent((ComparisonType) decoration, builder);
+        } else if (decoration instanceof CompoundType) {
+            ToXContent((CompoundType) decoration, builder);
+        } else if (decoration instanceof UpcastPainlessCast) {
+            ToXContent((UpcastPainlessCast) decoration, builder);
+        } else if (decoration instanceof DowncastPainlessCast) {
+            ToXContent((DowncastPainlessCast) decoration, builder);
+        } else if (decoration instanceof StandardPainlessField) {
+            ToXContent((StandardPainlessField) decoration, builder);
+        } else if (decoration instanceof StandardPainlessConstructor) {
+            ToXContent((StandardPainlessConstructor) decoration, builder);
+        } else if (decoration instanceof StandardPainlessMethod) {
+            ToXContent((StandardPainlessMethod) decoration, builder);
+        } else if (decoration instanceof GetterPainlessMethod) {
+            ToXContent((GetterPainlessMethod) decoration, builder);
+        } else if (decoration instanceof SetterPainlessMethod) {
+            ToXContent((SetterPainlessMethod) decoration, builder);
+        } else if (decoration instanceof StandardConstant) {
+            ToXContent((StandardConstant) decoration, builder);
+        } else if (decoration instanceof StandardLocalFunction) {
+            ToXContent((StandardLocalFunction) decoration, builder);
+        } else if (decoration instanceof StandardPainlessClassBinding) {
+            ToXContent((StandardPainlessClassBinding) decoration, builder);
+        } else if (decoration instanceof StandardPainlessInstanceBinding) {
+            ToXContent((StandardPainlessInstanceBinding) decoration, builder);
+        } else if (decoration instanceof MethodNameDecoration) {
+            ToXContent((MethodNameDecoration) decoration, builder);
+        } else if (decoration instanceof ReturnType) {
+            ToXContent((ReturnType) decoration, builder);
+        } else if (decoration instanceof TypeParameters) {
+            ToXContent((TypeParameters) decoration, builder);
+        } else if (decoration instanceof ParameterNames) {
+            ToXContent((ParameterNames) decoration, builder);
+        } else if (decoration instanceof ReferenceDecoration) {
+            ToXContent((ReferenceDecoration) decoration, builder);
+        } else if (decoration instanceof EncodingDecoration) {
+            ToXContent((EncodingDecoration) decoration, builder);
+        } else if (decoration instanceof CapturesDecoration) {
+            ToXContent((CapturesDecoration) decoration, builder);
+        } else if (decoration instanceof InstanceType) {
+            ToXContent((InstanceType) decoration, builder);
+        } else if (decoration instanceof AccessDepth) {
+            ToXContent((AccessDepth) decoration, builder);
+        } else if (decoration instanceof IRNodeDecoration) {
+            ToXContent((IRNodeDecoration) decoration, builder);
+        } else if (decoration instanceof Converter) {
+            ToXContent((Converter) decoration, builder);
+        } else {
+            builder.startObject();
+            builder.field(Fields.DECORATION, decoration.getClass().getSimpleName());
+            builder.endObject();
+        }
+    }
+
+    // lookup
+    public static void ToXContent(PainlessCast painlessCast, XContentBuilderWrapper builder) {
+        builder.startObject();
+        if (painlessCast.originalType != null) {
+            builder.field("originalType", painlessCast.originalType.getSimpleName());
+        }
+        if (painlessCast.targetType != null) {
+            builder.field("targetType", painlessCast.targetType.getSimpleName());
+        }
+
+        builder.field("explicitCast", painlessCast.explicitCast);
+
+        if (painlessCast.unboxOriginalType != null) {
+            builder.field("unboxOriginalType", painlessCast.unboxOriginalType.getSimpleName());
+        }
+        if (painlessCast.unboxTargetType != null) {
+            builder.field("unboxTargetType", painlessCast.unboxTargetType.getSimpleName());
+        }
+        if (painlessCast.boxOriginalType != null) {
+            builder.field("boxOriginalType", painlessCast.boxOriginalType.getSimpleName());
+        }
+        builder.endObject();
+    }
+
+    public static void ToXContent(PainlessMethod method, XContentBuilderWrapper builder) {
+        builder.startObject();
+        if (method.javaMethod != null) {
+            builder.field("javaMethod");
+            ToXContent(method.methodType, builder);
+        }
+        if (method.targetClass != null) {
+            builder.field("targetClass", method.targetClass.getSimpleName());
+        }
+        if (method.returnType != null) {
+            builder.field("returnType", method.returnType.getSimpleName());
+        }
+        if (method.typeParameters != null && method.typeParameters.isEmpty() == false) {
+            builder.field("typeParameters", classNames(method.typeParameters));
+        }
+        if (method.methodHandle != null) {
+            builder.field("methodHandle");
+            ToXContent(method.methodHandle.type(), builder);
+        }
+        // ignoring methodType as that's handled under methodHandle
+        AnnotationsToXContent(method.annotations, builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(FunctionTable.LocalFunction localFunction, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("functionName", localFunction.getFunctionName());
+        builder.field("returnType", localFunction.getReturnType().getSimpleName());
+        if (localFunction.getTypeParameters().isEmpty() == false) {
+            builder.field("typeParameters", classNames(localFunction.getTypeParameters()));
+        }
+        builder.field("isInternal", localFunction.isInternal());
+        builder.field("isStatic", localFunction.isStatic());
+        builder.field("methodType");
+        ToXContent(localFunction.getMethodType(), builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(PainlessClassBinding binding, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("javaConstructor");
+        ToXContent(binding.javaConstructor, builder);
+
+        builder.field("javaMethod");
+        ToXContent(binding.javaMethod, builder);
+        builder.field("returnType", binding.returnType.getSimpleName());
+        if (binding.typeParameters.isEmpty() == false) {
+            builder.field("typeParameters", classNames(binding.typeParameters));
+        }
+        AnnotationsToXContent(binding.annotations, builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(PainlessInstanceBinding binding, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("targetInstance", binding.targetInstance.getClass().getSimpleName());
+
+        builder.field("javaMethod");
+        ToXContent(binding.javaMethod, builder);
+        builder.field("returnType", binding.returnType.getSimpleName());
+        if (binding.typeParameters.isEmpty() == false) {
+            builder.field("typeParameters", classNames(binding.typeParameters));
+        }
+        AnnotationsToXContent(binding.annotations, builder);
+        builder.endObject();
+    }
+
+    public static void ToXContent(PainlessField field, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("javaField");
+        ToXContent(field.javaField, builder);
+        builder.field("typeParameter", field.typeParameter.getSimpleName());
+        builder.field("getterMethodHandle");
+        ToXContent(field.getterMethodHandle.type(), builder);
+        builder.field("setterMethodHandle");
+        if (field.setterMethodHandle != null) {
+            ToXContent(field.setterMethodHandle.type(), builder);
+        }
+        builder.endObject();
+    }
+
+    public static void ToXContent(PainlessConstructor constructor, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("javaConstructor");
+        ToXContent(constructor.javaConstructor, builder);
+        if (constructor.typeParameters.isEmpty() == false) {
+            builder.field("typeParameters", classNames(constructor.typeParameters));
+        }
+        builder.field("methodHandle");
+        ToXContent(constructor.methodHandle.type(), builder);
+        builder.endObject();
+    }
+
+    // symbol
+    public static void ToXContent(SemanticScope.Variable variable, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field(Fields.TYPE, variable.getType());
+        builder.field("name", variable.getName());
+        builder.field("isFinal", variable.isFinal());
+        builder.endObject();
+    }
+
+    // annotations
+    public static void AnnotationsToXContent(Map<Class<?>, Object> annotations, XContentBuilderWrapper builder) {
+        if (annotations == null || annotations.isEmpty()) {
+            return;
+        }
+        builder.startArray("annotations");
+        for (Class<?> key : annotations.keySet().stream().sorted().collect(Collectors.toList())) {
+            AnnotationToXContent(annotations.get(key), builder);
+        }
+        builder.endArray();
+    }
+
+    public static void AnnotationToXContent(Object annotation, XContentBuilderWrapper builder) {
+        if (annotation instanceof CompileTimeOnlyAnnotation) {
+            builder.value(CompileTimeOnlyAnnotation.NAME);
+        } else if (annotation instanceof DeprecatedAnnotation) {
+            builder.startObject();
+            builder.field("name", DeprecatedAnnotation.NAME);
+            builder.field("message", ((DeprecatedAnnotation) annotation).getMessage());
+            builder.endObject();
+        } else if (annotation instanceof InjectConstantAnnotation) {
+            builder.startObject();
+            builder.field("name", InjectConstantAnnotation.NAME);
+            builder.field("message", ((InjectConstantAnnotation) annotation).injects);
+            builder.endObject();
+        } else if (annotation instanceof NoImportAnnotation) {
+            builder.value(NoImportAnnotation.NAME);
+        } else if (annotation instanceof NonDeterministicAnnotation) {
+            builder.value(NonDeterministicAnnotation.NAME);
+        } else {
+            builder.value(annotation.toString());
+        }
+    }
+
+    // asm
+    public static void ToXContent(org.objectweb.asm.commons.Method asmMethod, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("name", asmMethod.getName());
+        builder.field("descriptor", asmMethod.getDescriptor());
+        builder.field("returnType", asmMethod.getReturnType().getClassName());
+        builder.field("argumentTypes", Arrays.stream(asmMethod.getArgumentTypes()).map(Type::getClassName));
+        builder.endObject();
+    }
+
+    // java.lang.invoke
+    public static void ToXContent(MethodType methodType, XContentBuilderWrapper builder) {
+        builder.startObject();
+        List<Class<?>> parameters = methodType.parameterList();
+        if (parameters.isEmpty() == false) {
+            builder.field("parameters", classNames(parameters));
+        }
+        builder.field("return", methodType.returnType().getSimpleName());
+        builder.endObject();
+    }
+
+    // java.lang.reflect
+    public static void ToXContent(Field field, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("name", field.getName());
+        builder.field("type", field.getType().getSimpleName());
+        builder.field("modifiers", Modifier.toString(field.getModifiers()));
+        builder.endObject();
+    }
+
+    public static void ToXContent(Method method, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("name", method.getName());
+        builder.field("parameters", classNames(method.getParameterTypes()));
+        builder.field("return", method.getReturnType().getSimpleName());
+        Class<?>[] exceptions = method.getExceptionTypes();
+        if (exceptions.length > 0) {
+            builder.field("exceptions", classNames(exceptions));
+        }
+        builder.field("modifiers", Modifier.toString(method.getModifiers()));
+        builder.endObject();
+    }
+
+    public static void ToXContent(Constructor<?> constructor, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field("name", constructor.getName());
+        if (constructor.getParameterTypes().length > 0) {
+            builder.field("parameterTypes", classNames(constructor.getParameterTypes()));
+        }
+        if (constructor.getExceptionTypes().length > 0) {
+            builder.field("exceptionTypes", classNames(constructor.getExceptionTypes()));
+        }
+        builder.field("modifiers", Modifier.toString(constructor.getModifiers()));
+        builder.endObject();
+    }
+
+    // helpers
+    public static void start(Decoration decoration, XContentBuilderWrapper builder) {
+        builder.startObject();
+        builder.field(Fields.DECORATION, decoration.getClass().getSimpleName());
+    }
+
+    public static List<String> classNames(Class<?>[] classes) {
+        return Arrays.stream(classes).map(Class::getSimpleName).collect(Collectors.toList());
+    }
+
+    public static List<String> classNames(List<Class<?>> classes) {
+        return classes.stream().map(Class::getSimpleName).collect(Collectors.toList());
+    }
+}

+ 688 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/toxcontent/UserTreeToXContent.java

@@ -0,0 +1,688 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.painless.toxcontent;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.painless.Operation;
+import org.elasticsearch.painless.node.AExpression;
+import org.elasticsearch.painless.node.ANode;
+import org.elasticsearch.painless.node.EAssignment;
+import org.elasticsearch.painless.node.EBinary;
+import org.elasticsearch.painless.node.EBooleanComp;
+import org.elasticsearch.painless.node.EBooleanConstant;
+import org.elasticsearch.painless.node.EBrace;
+import org.elasticsearch.painless.node.ECall;
+import org.elasticsearch.painless.node.ECallLocal;
+import org.elasticsearch.painless.node.EComp;
+import org.elasticsearch.painless.node.EConditional;
+import org.elasticsearch.painless.node.EDecimal;
+import org.elasticsearch.painless.node.EDot;
+import org.elasticsearch.painless.node.EElvis;
+import org.elasticsearch.painless.node.EExplicit;
+import org.elasticsearch.painless.node.EFunctionRef;
+import org.elasticsearch.painless.node.EInstanceof;
+import org.elasticsearch.painless.node.ELambda;
+import org.elasticsearch.painless.node.EListInit;
+import org.elasticsearch.painless.node.EMapInit;
+import org.elasticsearch.painless.node.ENewArray;
+import org.elasticsearch.painless.node.ENewArrayFunctionRef;
+import org.elasticsearch.painless.node.ENewObj;
+import org.elasticsearch.painless.node.ENull;
+import org.elasticsearch.painless.node.ENumeric;
+import org.elasticsearch.painless.node.ERegex;
+import org.elasticsearch.painless.node.EString;
+import org.elasticsearch.painless.node.ESymbol;
+import org.elasticsearch.painless.node.EUnary;
+import org.elasticsearch.painless.node.SBlock;
+import org.elasticsearch.painless.node.SBreak;
+import org.elasticsearch.painless.node.SCatch;
+import org.elasticsearch.painless.node.SClass;
+import org.elasticsearch.painless.node.SContinue;
+import org.elasticsearch.painless.node.SDeclBlock;
+import org.elasticsearch.painless.node.SDeclaration;
+import org.elasticsearch.painless.node.SDo;
+import org.elasticsearch.painless.node.SEach;
+import org.elasticsearch.painless.node.SExpression;
+import org.elasticsearch.painless.node.SFor;
+import org.elasticsearch.painless.node.SFunction;
+import org.elasticsearch.painless.node.SIf;
+import org.elasticsearch.painless.node.SIfElse;
+import org.elasticsearch.painless.node.SReturn;
+import org.elasticsearch.painless.node.SThrow;
+import org.elasticsearch.painless.node.STry;
+import org.elasticsearch.painless.node.SWhile;
+import org.elasticsearch.painless.phase.UserTreeBaseVisitor;
+import org.elasticsearch.painless.symbol.Decorator.Condition;
+import org.elasticsearch.painless.symbol.Decorator.Decoration;
+import org.elasticsearch.painless.symbol.ScriptScope;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Serialize the user tree
+ */
+public class UserTreeToXContent extends UserTreeBaseVisitor<ScriptScope> {
+    public final XContentBuilderWrapper builder;
+
+    public UserTreeToXContent(XContentBuilder builder) {
+        this.builder = new XContentBuilderWrapper(Objects.requireNonNull(builder));
+    }
+
+    public UserTreeToXContent() {
+        this.builder = new XContentBuilderWrapper();
+    }
+
+    static final class Fields {
+        static final String NODE = "node";
+        static final String LOCATION = "location";
+        static final String LEFT = "left";
+        static final String RIGHT = "right";
+        static final String BLOCK = "block";
+        static final String CONDITION = "condition";
+        static final String TYPE = "type";
+        static final String SYMBOL = "symbol";
+        static final String DECORATIONS = "decorations";
+        static final String CONDITIONS = "conditions";
+    }
+
+    @Override
+    public void visitClass(SClass userClassNode, ScriptScope scope) {
+        start(userClassNode);
+
+        builder.field("source", scope.getScriptSource());
+        builder.startArray("functions");
+        userClassNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userClassNode, scope);
+    }
+
+    @Override
+    public void visitFunction(SFunction userFunctionNode, ScriptScope scope) {
+        start(userFunctionNode);
+
+        builder.field("name", userFunctionNode.getFunctionName());
+        builder.field("returns", userFunctionNode.getReturnCanonicalTypeName());
+        if (userFunctionNode.getParameterNames().isEmpty() == false) {
+            builder.field("parameters", userFunctionNode.getParameterNames());
+        }
+        if (userFunctionNode.getCanonicalTypeNameParameters().isEmpty() == false) {
+            builder.field("parameterTypes", userFunctionNode.getCanonicalTypeNameParameters());
+        }
+        builder.field("isInternal", userFunctionNode.isInternal());
+        builder.field("isStatic", userFunctionNode.isStatic());
+        builder.field("isSynthetic", userFunctionNode.isSynthetic());
+        builder.field("isAutoReturnEnabled", userFunctionNode.isAutoReturnEnabled());
+
+        builder.startArray(Fields.BLOCK);
+        userFunctionNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userFunctionNode, scope);
+    }
+
+    @Override
+    public void visitBlock(SBlock userBlockNode, ScriptScope scope) {
+        start(userBlockNode);
+
+        builder.startArray("statements");
+        userBlockNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userBlockNode, scope);
+    }
+
+    @Override
+    public void visitIf(SIf userIfNode, ScriptScope scope) {
+        start(userIfNode);
+
+        builder.startArray(Fields.CONDITION);
+        userIfNode.getConditionNode().visit(this, scope);
+        builder.endArray();
+
+        block("ifBlock", userIfNode.getIfBlockNode(), scope);
+
+        end(userIfNode, scope);
+    }
+
+    @Override
+    public void visitIfElse(SIfElse userIfElseNode, ScriptScope scope) {
+        start(userIfElseNode);
+
+        builder.startArray(Fields.CONDITION);
+        userIfElseNode.getConditionNode().visit(this, scope);
+        builder.endArray();
+
+        block("ifBlock", userIfElseNode.getIfBlockNode(), scope);
+        block("elseBlock", userIfElseNode.getElseBlockNode(), scope);
+
+        end(userIfElseNode, scope);
+    }
+
+    @Override
+    public void visitWhile(SWhile userWhileNode, ScriptScope scope) {
+        start(userWhileNode);
+        loop(userWhileNode.getConditionNode(), userWhileNode.getBlockNode(), scope);
+        end(userWhileNode, scope);
+    }
+
+    @Override
+    public void visitDo(SDo userDoNode, ScriptScope scope) {
+        start(userDoNode);
+        loop(userDoNode.getConditionNode(), userDoNode.getBlockNode(), scope);
+        end(userDoNode, scope);
+    }
+
+    @Override
+    public void visitFor(SFor userForNode, ScriptScope scope) {
+        start(userForNode);
+
+        ANode initializerNode = userForNode.getInitializerNode();
+        builder.startArray("initializer");
+        if (initializerNode != null) {
+            initializerNode.visit(this, scope);
+        }
+        builder.endArray();
+
+        builder.startArray("condition");
+        AExpression conditionNode = userForNode.getConditionNode();
+        if (conditionNode != null) {
+            conditionNode.visit(this, scope);
+        }
+        builder.endArray();
+
+        builder.startArray("afterthought");
+        AExpression afterthoughtNode = userForNode.getAfterthoughtNode();
+        if (afterthoughtNode != null) {
+            afterthoughtNode.visit(this, scope);
+        }
+        builder.endArray();
+
+        block(userForNode.getBlockNode(), scope);
+
+        end(userForNode, scope);
+    }
+
+    @Override
+    public void visitEach(SEach userEachNode, ScriptScope scope) {
+        start(userEachNode);
+
+        builder.field(Fields.TYPE, userEachNode.getCanonicalTypeName());
+        builder.field(Fields.SYMBOL, userEachNode.getSymbol());
+
+        builder.startArray("iterable");
+        userEachNode.getIterableNode().visitChildren(this, scope);
+        builder.endArray();
+
+        block(userEachNode.getBlockNode(), scope);
+
+        end(userEachNode, scope);
+    }
+
+    @Override
+    public void visitDeclBlock(SDeclBlock userDeclBlockNode, ScriptScope scope) {
+        start(userDeclBlockNode);
+
+        builder.startArray("declarations");
+        userDeclBlockNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userDeclBlockNode, scope);
+    }
+
+    @Override
+    public void visitDeclaration(SDeclaration userDeclarationNode, ScriptScope scope) {
+        start(userDeclarationNode);
+
+        builder.field(Fields.TYPE, userDeclarationNode.getCanonicalTypeName());
+        builder.field(Fields.SYMBOL, userDeclarationNode.getSymbol());
+
+        builder.startArray("value");
+        userDeclarationNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userDeclarationNode, scope);
+    }
+
+    @Override
+    public void visitReturn(SReturn userReturnNode, ScriptScope scope) {
+        start(userReturnNode);
+
+        builder.startArray("value");
+        userReturnNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userReturnNode, scope);
+    }
+
+    @Override
+    public void visitExpression(SExpression userExpressionNode, ScriptScope scope) {
+        start(userExpressionNode);
+
+        builder.startArray("statement");
+        userExpressionNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userExpressionNode, scope);
+    }
+
+    @Override
+    public void visitTry(STry userTryNode, ScriptScope scope) {
+        start(userTryNode);
+
+        block(userTryNode.getBlockNode(), scope);
+
+        builder.startArray("catch");
+        for (SCatch catchNode : userTryNode.getCatchNodes()) {
+            catchNode.visit(this, scope);
+        }
+        builder.endArray();
+
+        end(userTryNode, scope);
+    }
+
+    @Override
+    public void visitCatch(SCatch userCatchNode, ScriptScope scope) {
+        start(userCatchNode);
+
+        builder.field("exception", userCatchNode.getBaseException());
+        builder.field(Fields.TYPE, userCatchNode.getCanonicalTypeName());
+        builder.field(Fields.SYMBOL, userCatchNode.getSymbol());
+
+        builder.startArray(Fields.BLOCK);
+        userCatchNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userCatchNode, scope);
+    }
+
+    @Override
+    public void visitThrow(SThrow userThrowNode, ScriptScope scope) {
+        start(userThrowNode);
+
+        builder.startArray("expression");
+        userThrowNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userThrowNode, scope);
+    }
+
+    @Override
+    public void visitContinue(SContinue userContinueNode, ScriptScope scope) {
+        start(userContinueNode);
+        end(userContinueNode, scope);
+    }
+
+    @Override
+    public void visitBreak(SBreak userBreakNode, ScriptScope scope) {
+        start(userBreakNode);
+        end(userBreakNode, scope);
+    }
+
+    @Override
+    public void visitAssignment(EAssignment userAssignmentNode, ScriptScope scope) {
+        start(userAssignmentNode);
+        // TODO(stu): why would operation be null?
+        builder.field("postIfRead", userAssignmentNode.postIfRead());
+        binaryOperation(userAssignmentNode.getOperation(), userAssignmentNode.getLeftNode(), userAssignmentNode.getRightNode(), scope);
+        end(userAssignmentNode, scope);
+    }
+
+    @Override
+    public void visitUnary(EUnary userUnaryNode, ScriptScope scope) {
+        start(userUnaryNode);
+
+        operation(userUnaryNode.getOperation());
+
+        builder.startArray("child");
+        userUnaryNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userUnaryNode, scope);
+    }
+
+    @Override
+    public void visitBinary(EBinary userBinaryNode, ScriptScope scope) {
+        start(userBinaryNode);
+        binaryOperation(userBinaryNode.getOperation(), userBinaryNode.getLeftNode(), userBinaryNode.getRightNode(), scope);
+        end(userBinaryNode, scope);
+    }
+
+    @Override
+    public void visitBooleanComp(EBooleanComp userBooleanCompNode, ScriptScope scope) {
+        start(userBooleanCompNode);
+        binaryOperation(userBooleanCompNode.getOperation(), userBooleanCompNode.getLeftNode(), userBooleanCompNode.getRightNode(), scope);
+        end(userBooleanCompNode, scope);
+    }
+
+    @Override
+    public void visitComp(EComp userCompNode, ScriptScope scope) {
+        start(userCompNode);
+        binaryOperation(userCompNode.getOperation(), userCompNode.getLeftNode(), userCompNode.getRightNode(), scope);
+        end(userCompNode, scope);
+    }
+
+    @Override
+    public void visitExplicit(EExplicit userExplicitNode, ScriptScope scope) {
+        start(userExplicitNode);
+
+        builder.field(Fields.TYPE, userExplicitNode.getCanonicalTypeName());
+        builder.startArray("child");
+        userExplicitNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userExplicitNode, scope);
+    }
+
+    @Override
+    public void visitInstanceof(EInstanceof userInstanceofNode, ScriptScope scope) {
+        start(userInstanceofNode);
+
+        builder.field(Fields.TYPE, userInstanceofNode.getCanonicalTypeName());
+        builder.startArray("child");
+        userInstanceofNode.visitChildren(this, scope);
+        builder.endArray();
+
+        end(userInstanceofNode, scope);
+    }
+
+    @Override
+    public void visitConditional(EConditional userConditionalNode, ScriptScope scope) {
+        start(userConditionalNode);
+
+        builder.startArray("condition");
+        userConditionalNode.getConditionNode().visit(this, scope);
+        builder.endArray();
+
+        builder.startArray("true");
+        userConditionalNode.getTrueNode().visit(this, scope);
+        builder.endArray();
+
+        builder.startArray("false");
+        userConditionalNode.getFalseNode().visit(this, scope);
+        builder.endArray();
+
+        end(userConditionalNode, scope);
+    }
+
+    @Override
+    public void visitElvis(EElvis userElvisNode, ScriptScope scope) {
+        start(userElvisNode);
+
+        builder.startArray(Fields.LEFT);
+        userElvisNode.getLeftNode().visit(this, scope);
+        builder.endArray();
+
+        builder.startArray(Fields.RIGHT);
+        userElvisNode.getRightNode().visit(this, scope);
+        builder.endArray();
+
+        end(userElvisNode, scope);
+    }
+
+    @Override
+    public void visitListInit(EListInit userListInitNode, ScriptScope scope) {
+        start(userListInitNode);
+        builder.startArray("values");
+        userListInitNode.visitChildren(this, scope);
+        builder.endArray();
+        end(userListInitNode, scope);
+    }
+
+    @Override
+    public void visitMapInit(EMapInit userMapInitNode, ScriptScope scope) {
+        start(userMapInitNode);
+        expressions("keys", userMapInitNode.getKeyNodes(), scope);
+        expressions("values", userMapInitNode.getValueNodes(), scope);
+        end(userMapInitNode, scope);
+    }
+
+    @Override
+    public void visitNewArray(ENewArray userNewArrayNode, ScriptScope scope) {
+        start(userNewArrayNode);
+        builder.field(Fields.TYPE, userNewArrayNode.getCanonicalTypeName());
+        builder.field("isInitializer", userNewArrayNode.isInitializer());
+        expressions("values", userNewArrayNode.getValueNodes(), scope);
+        end(userNewArrayNode, scope);
+    }
+
+    @Override
+    public void visitNewObj(ENewObj userNewObjNode, ScriptScope scope) {
+        start(userNewObjNode);
+        builder.field(Fields.TYPE, userNewObjNode.getCanonicalTypeName());
+        arguments(userNewObjNode.getArgumentNodes(), scope);
+        end(userNewObjNode, scope);
+    }
+
+    @Override
+    public void visitCallLocal(ECallLocal userCallLocalNode, ScriptScope scope) {
+        start(userCallLocalNode);
+        builder.field("methodName", userCallLocalNode.getMethodName());
+        arguments(userCallLocalNode.getArgumentNodes(), scope);
+        end(userCallLocalNode, scope);
+    }
+
+    @Override
+    public void visitBooleanConstant(EBooleanConstant userBooleanConstantNode, ScriptScope scope) {
+        start(userBooleanConstantNode);
+        builder.field("value", userBooleanConstantNode.getBool());
+        end(userBooleanConstantNode, scope);
+    }
+
+    @Override
+    public void visitNumeric(ENumeric userNumericNode, ScriptScope scope) {
+        start(userNumericNode);
+        builder.field("numeric", userNumericNode.getNumeric());
+        builder.field("radix", userNumericNode.getRadix());
+        end(userNumericNode, scope);
+    }
+
+    @Override
+    public void visitDecimal(EDecimal userDecimalNode, ScriptScope scope) {
+        start(userDecimalNode);
+        builder.field("value", userDecimalNode.getDecimal());
+        end(userDecimalNode, scope);
+    }
+
+    @Override
+    public void visitString(EString userStringNode, ScriptScope scope) {
+        start(userStringNode);
+        builder.field("value", userStringNode.getString());
+        end(userStringNode, scope);
+    }
+
+    @Override
+    public void visitNull(ENull userNullNode, ScriptScope scope) {
+        start(userNullNode);
+        end(userNullNode, scope);
+    }
+
+    @Override
+    public void visitRegex(ERegex userRegexNode, ScriptScope scope) {
+        start(userRegexNode);
+        builder.field("pattern", userRegexNode.getPattern());
+        builder.field("flags", userRegexNode.getFlags());
+        end(userRegexNode, scope);
+    }
+
+    @Override
+    public void visitLambda(ELambda userLambdaNode, ScriptScope scope) {
+        start(userLambdaNode);
+        builder.field("types", userLambdaNode.getCanonicalTypeNameParameters());
+        builder.field("parameters", userLambdaNode.getParameterNames());
+        block(userLambdaNode.getBlockNode(), scope);
+        end(userLambdaNode, scope);
+    }
+
+    @Override
+    public void visitFunctionRef(EFunctionRef userFunctionRefNode, ScriptScope scope) {
+        start(userFunctionRefNode);
+        builder.field(Fields.SYMBOL, userFunctionRefNode.getSymbol());
+        builder.field("methodName", userFunctionRefNode.getMethodName());
+        end(userFunctionRefNode, scope);
+    }
+
+    @Override
+    public void visitNewArrayFunctionRef(ENewArrayFunctionRef userNewArrayFunctionRefNode, ScriptScope scope) {
+        start(userNewArrayFunctionRefNode);
+        builder.field(Fields.TYPE, userNewArrayFunctionRefNode.getCanonicalTypeName());
+        end(userNewArrayFunctionRefNode, scope);
+    }
+
+    @Override
+    public void visitSymbol(ESymbol userSymbolNode, ScriptScope scope) {
+        start(userSymbolNode);
+        builder.field(Fields.SYMBOL, userSymbolNode.getSymbol());
+        end(userSymbolNode, scope);
+    }
+
+    @Override
+    public void visitDot(EDot userDotNode, ScriptScope scope) {
+        start(userDotNode);
+
+        builder.startArray("prefix");
+        userDotNode.visitChildren(this, scope);
+        builder.endArray();
+
+        builder.field("index", userDotNode.getIndex());
+        builder.field("nullSafe", userDotNode.isNullSafe());
+
+        end(userDotNode, scope);
+    }
+
+    @Override
+    public void visitBrace(EBrace userBraceNode, ScriptScope scope) {
+        start(userBraceNode);
+
+        builder.startArray("prefix");
+        userBraceNode.getPrefixNode().visit(this, scope);
+        builder.endArray();
+
+        builder.startArray("index");
+        userBraceNode.getIndexNode().visit(this, scope);
+        builder.endArray();
+
+        end(userBraceNode, scope);
+    }
+
+    @Override
+    public void visitCall(ECall userCallNode, ScriptScope scope) {
+        start(userCallNode);
+
+        builder.startArray("prefix");
+        userCallNode.getPrefixNode().visitChildren(this, scope);
+        builder.endArray();
+
+        builder.field("isNullSafe", userCallNode.isNullSafe());
+        builder.field("methodName", userCallNode.getMethodName());
+
+        arguments(userCallNode.getArgumentNodes(), scope);
+
+        end(userCallNode, scope);
+    }
+
+    private void start(ANode node) {
+        builder.startObject();
+        builder.field(Fields.NODE, node.getClass().getSimpleName());
+        builder.field(Fields.LOCATION, node.getLocation().getOffset());
+    }
+
+    private void end(ANode node, ScriptScope scope) {
+        decorations(node, scope);
+        builder.endObject();
+    }
+
+    private void block(String name, SBlock block, ScriptScope scope) {
+        builder.startArray(name);
+        if (block != null) {
+            block.visit(this, scope);
+        }
+        builder.endArray();
+    }
+
+    private void block(SBlock block, ScriptScope scope) {
+        block(Fields.BLOCK, block, scope);
+    }
+
+    private void loop(AExpression condition, SBlock block, ScriptScope scope) {
+        builder.startArray(Fields.CONDITION);
+        condition.visit(this, scope);
+        builder.endArray();
+
+        block(block, scope);
+    }
+
+    private void operation(Operation op) {
+        builder.startObject("operation");
+        if (op != null) {
+            builder.field(Fields.SYMBOL, op.symbol);
+            builder.field("name", op.name);
+        }
+        builder.endObject();
+    }
+
+    private void binaryOperation(Operation op, AExpression left, AExpression right, ScriptScope scope) {
+        operation(op);
+
+        builder.startArray(Fields.LEFT);
+        left.visit(this, scope);
+        builder.endArray();
+
+        builder.startArray(Fields.RIGHT);
+        right.visit(this, scope);
+        builder.endArray();
+    }
+
+    private void arguments(List<AExpression> arguments, ScriptScope scope) {
+        if (arguments.isEmpty() == false) {
+            expressions("arguments", arguments, scope);
+        }
+    }
+
+    private void expressions(String name, List<AExpression> expressions, ScriptScope scope) {
+        if (expressions.isEmpty() == false) {
+            builder.startArray(name);
+            for (AExpression expression : expressions) {
+                expression.visit(this, scope);
+            }
+            builder.endArray();
+        }
+    }
+
+    private void decorations(ANode node, ScriptScope scope) {
+        Set<Class<? extends Condition>> conditions = scope.getAllConditions(node.getIdentifier());
+        if (conditions.isEmpty() == false) {
+            builder.field(Fields.CONDITIONS, conditions.stream().map(Class::getSimpleName).sorted().collect(Collectors.toList()));
+        }
+
+        Map<Class<? extends Decoration>, Decoration> decorations = scope.getAllDecorations(node.getIdentifier());
+        if (decorations.isEmpty() == false) {
+            builder.startArray(Fields.DECORATIONS);
+
+            List<Class<? extends Decoration>> dkeys = decorations.keySet().stream()
+                    .sorted(Comparator.comparing(Class::getName))
+                    .collect(Collectors.toList());
+
+            for (Class<? extends Decoration> dkey : dkeys) {
+                DecorationToXContent.ToXContent(decorations.get(dkey), builder);
+            }
+            builder.endArray();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return builder.toString();
+    }
+}

+ 159 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/toxcontent/XContentBuilderWrapper.java

@@ -0,0 +1,159 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.painless.toxcontent;
+
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public class XContentBuilderWrapper {
+    public final XContentBuilder builder;
+
+    public XContentBuilderWrapper(XContentBuilder builder) {
+        this.builder = Objects.requireNonNull(builder);
+    }
+
+    public XContentBuilderWrapper() {
+        XContentBuilder jsonBuilder;
+        try {
+            jsonBuilder = XContentFactory.jsonBuilder();
+        } catch (IOException io) {
+            throw new RuntimeException(io);
+        }
+        this.builder = jsonBuilder.prettyPrint();
+    }
+
+    public void startObject() {
+        try {
+            builder.startObject();
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void startObject(String name)  {
+        try {
+            builder.startObject(name);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void endObject() {
+        try {
+            builder.endObject();
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void startArray() {
+        try {
+            builder.startArray();
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void startArray(String name) {
+        try {
+            builder.startArray(name);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void endArray() {
+        try {
+            builder.endArray();
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name)  {
+        try {
+            builder.field(name);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name, Object value)  {
+        try {
+            if (value instanceof Character) {
+                builder.field(name, ((Character) value).charValue());
+            } else if (value instanceof Pattern) {
+                // This does not serialize the flags
+                builder.field(name, ((Pattern) value).pattern());
+            } else {
+                builder.field(name, value);
+            }
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name, String value)  {
+        try {
+            builder.field(name, value);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name, Class<?> value)  {
+        field(name, value.getName());
+    }
+
+    public void field(String name, int value)  {
+        try {
+            builder.field(name, value);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name, boolean value)  {
+        try {
+            builder.field(name, value);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void field(String name, List<String> values)  {
+        try {
+            builder.field(name, values);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public void value(String value) {
+        try {
+            builder.value(value);
+        } catch (IOException io) {
+            throw new IllegalStateException(io);
+        }
+    }
+
+    public String toString() {
+        try {
+            builder.flush();
+        } catch (IOException io) {
+            throw new RuntimeException(io);
+        }
+        return builder.getOutputStream().toString();
+    }
+}

+ 30 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/Debugger.java

@@ -10,7 +10,11 @@ package org.elasticsearch.painless;
 
 import org.elasticsearch.painless.action.PainlessExecuteAction.PainlessTestScript;
 import org.elasticsearch.painless.lookup.PainlessLookupBuilder;
+import org.elasticsearch.painless.phase.IRTreeVisitor;
+import org.elasticsearch.painless.phase.UserTreeVisitor;
 import org.elasticsearch.painless.spi.Whitelist;
+import org.elasticsearch.painless.symbol.ScriptScope;
+import org.elasticsearch.painless.symbol.WriteScope;
 import org.objectweb.asm.util.Textifier;
 
 import java.io.PrintWriter;
@@ -42,4 +46,30 @@ final class Debugger {
         textifier.print(outputWriter);
         return output.toString();
     }
+
+    /** compiles to bytecode, and returns debugging output */
+    private static String tree(Class<?> iface, String source, CompilerSettings settings, List<Whitelist> whitelists,
+                               UserTreeVisitor<ScriptScope> semanticPhaseVisitor, UserTreeVisitor<ScriptScope> irPhaseVisitor,
+                               IRTreeVisitor<WriteScope> asmPhaseVisitor) {
+        StringWriter output = new StringWriter();
+        PrintWriter outputWriter = new PrintWriter(output);
+        Textifier textifier = new Textifier();
+        try {
+            new Compiler(iface, null, null, PainlessLookupBuilder.buildFromWhitelists(whitelists))
+                    .compile("<debugging>", source, settings, textifier, semanticPhaseVisitor, irPhaseVisitor, asmPhaseVisitor);
+        } catch (RuntimeException e) {
+            textifier.print(outputWriter);
+            e.addSuppressed(new Exception("current bytecode: \n" + output));
+            throw e;
+        }
+
+        textifier.print(outputWriter);
+        return output.toString();
+    }
+
+    static void phases(final String source, UserTreeVisitor<ScriptScope> semanticPhaseVisitor, UserTreeVisitor<ScriptScope> irPhaseVisitor,
+                       IRTreeVisitor<WriteScope> asmPhaseVisitor) {
+        tree(PainlessTestScript.class, source, new CompilerSettings(), Whitelist.BASE_WHITELISTS, semanticPhaseVisitor, irPhaseVisitor,
+             asmPhaseVisitor);
+    }
 }

+ 124 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/ToXContentTests.java

@@ -0,0 +1,124 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.painless;
+
+import org.elasticsearch.common.bytes.BytesReference;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.common.xcontent.XContentHelper;
+import org.elasticsearch.painless.phase.UserTreeVisitor;
+import org.elasticsearch.painless.symbol.ScriptScope;
+import org.elasticsearch.painless.toxcontent.UserTreeToXContent;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+public class ToXContentTests extends ScriptTestCase {
+    public void testUserFunction() {
+        Map<?,?> func = getFunction("def twofive(int i) { return 25 + i; } int j = 23; twofive(j)", "twofive");
+        assertFalse((Boolean)func.get("isInternal"));
+        assertTrue((Boolean)func.get("isStatic"));
+        assertEquals("SFunction", func.get("node"));
+        assertEquals("def", func.get("returns"));
+        assertEquals(List.of("int"), func.get("parameterTypes"));
+        assertEquals(List.of("i"), func.get("parameters"));
+    }
+
+    public void testBlock() {
+        Map<?, ?> execute = getExecute("int i = 5; return i;");
+        Map<?, ?> block = getNode(execute, "block", "SBlock");
+        for (Object obj : (List<?>) block.get("statements")) {
+            Map<?, ?> statement = (Map<?, ?>) obj;
+        }
+        Map<?, ?> decl = getStatement(block, "SDeclBlock");
+        List<?> decls = (List<?>) decl.get("declarations");
+        assertEquals(1, decls.size());
+        assertEquals("i", ((Map<?,?>) decls.get(0)).get("symbol"));
+        assertEquals("int", ((Map<?,?>) decls.get(0)).get("type"));
+
+        Map<?, ?> ret = getStatement(block, "SReturn");
+        Map<?, ?> symbol = (Map<?, ?>)((List<?>) ret.get("value")).get(0);
+        assertEquals("ESymbol", symbol.get("node"));
+        assertEquals("i", symbol.get("symbol"));
+    }
+
+    public void testFor() {
+        Map<?, ?> execute = getExecute("int q = 0; for (int j = 0; j < 100; j++) { q += j; } return q");
+        Map<?, ?> sfor = getStatement(getNode(execute, "block", "SBlock"), "SFor");
+
+        Map<?, ?> ecomp = getNode(sfor, "condition", "EComp");
+        assertEquals("j", getNode(ecomp, "left", "ESymbol").get("symbol"));
+        assertEquals("100", getNode(ecomp, "right", "ENumeric").get("numeric"));
+        assertEquals("less than", ((Map<?,?>) ecomp.get("operation")).get("name"));
+
+        Map<?, ?> init = getNode(sfor, "initializer", "SDeclBlock");
+        Map<?, ?> decl = getNode(init, "declarations", "SDeclaration");
+        assertEquals("j", decl.get("symbol"));
+        assertEquals("int", decl.get("type"));
+        assertEquals("0", getNode(decl, "value", "ENumeric").get("numeric"));
+
+        Map<?, ?> after = getNode(sfor, "afterthought", "EAssignment");
+        assertEquals("j", getNode(after, "left", "ESymbol").get("symbol"));
+        assertEquals("1", getNode(after, "right", "ENumeric").get("numeric"));
+        assertTrue((Boolean)after.get("postIfRead"));
+    }
+
+    private Map<?, ?> getStatement(Map<?, ?> block, String node) {
+        return getNode(block, "statements", node);
+    }
+
+    private Map<?, ?> getNode(Map<?, ?> map, String key, String node) {
+        for (Object obj : (List<?>) map.get(key)) {
+            Map<?, ?> nodeMap = (Map<?, ?>) obj;
+            if (node.equals(nodeMap.get("node"))) {
+                return nodeMap;
+            }
+        }
+        fail("Missing node [" + node + "]");
+        return Collections.emptyMap();
+    }
+
+    private Map<?, ?> getExecute(String script) {
+        return getFunction(script, "execute");
+    }
+
+    private Map<?, ?> getFunction(String script, String function) {
+        return getFunction(semanticPhase(script), function);
+    }
+
+    private Map<?, ?> getFunction(XContentBuilder builder, String function) {
+        Map<String, Object> map = XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2();
+        for (Object funcObj: ((List<?>)map.get("functions"))) {
+            if (funcObj instanceof Map) {
+                if (function.equals(((Map<?, ?>) funcObj).get("name"))) {
+                    return (Map<?, ?>) funcObj;
+                }
+            }
+        }
+        fail("Function [" + function + "] not found");
+        return Collections.emptyMap();
+    }
+
+    private XContentBuilder semanticPhase(String script) {
+        XContentBuilder builder;
+        try {
+            builder = XContentFactory.jsonBuilder();
+        } catch (IOException err) {
+            fail("script [" + script + "] threw IOException [" + err.getMessage() + "]");
+            return null;
+        }
+        UserTreeVisitor<ScriptScope> semantic = new UserTreeToXContent(builder);
+        Debugger.phases(script, semantic, null, null);
+        Map<String, Object> map = XContentHelper.convertToMap(BytesReference.bytes(builder), false, builder.contentType()).v2();
+        assertEquals(script, map.get("source"));
+        return builder;
+    }
+}

+ 16 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/UserFunctionTests.java

@@ -0,0 +1,16 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+package org.elasticsearch.painless;
+
+public class UserFunctionTests extends ScriptTestCase {
+    public void testZeroArgumentUserFunction() {
+        String source = "def twofive() { return 25; } twofive()";
+        assertEquals(25, exec(source));
+    }
+}