You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by am...@apache.org on 2021/03/17 10:13:06 UTC

[ignite-3] branch main updated: IGNITE-13618: Provide generated and reflection-based class (de)serializers. (#35)

This is an automated email from the ASF dual-hosted git repository.

amashenkov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new d5b50d7  IGNITE-13618: Provide generated and reflection-based class (de)serializers. (#35)
d5b50d7 is described below

commit d5b50d758df68131a5fcee40ca6e0da86cf6209e
Author: Andrew V. Mashenkov <AM...@users.noreply.github.com>
AuthorDate: Wed Mar 17 13:12:58 2021 +0300

    IGNITE-13618: Provide generated and reflection-based class (de)serializers. (#35)
---
 check-rules/pmd-rules.xml                          |  30 +-
 modules/README.md                                  |  12 +-
 modules/bytecode/README.md                         |   6 +
 pom.xml => modules/bytecode/pom.xml                |  46 +-
 .../java/com/facebook/presto/bytecode/Access.java  |  85 +++
 .../bytecode/AddFakeLineNumberClassVisitor.java    | 157 ++++
 .../presto/bytecode/AnnotationDefinition.java      | 211 ++++++
 .../com/facebook/presto/bytecode/ArrayOpCode.java  | 124 +++
 .../presto/bytecode/ByteCodeTooLargeException.java |  23 +
 .../facebook/presto/bytecode/BytecodeBlock.java    | 837 +++++++++++++++++++++
 .../com/facebook/presto/bytecode/BytecodeNode.java |  26 +
 .../facebook/presto/bytecode/BytecodeUtils.java    | 118 +++
 .../facebook/presto/bytecode/BytecodeVisitor.java  | 327 ++++++++
 .../facebook/presto/bytecode/ClassDefinition.java  | 281 +++++++
 .../facebook/presto/bytecode/ClassGenerator.java   | 204 +++++
 .../com/facebook/presto/bytecode/ClassInfo.java    | 169 +++++
 .../facebook/presto/bytecode/ClassInfoLoader.java  | 147 ++++
 .../java/com/facebook/presto/bytecode/Comment.java |  50 ++
 .../presto/bytecode/CompilationException.java      |  23 +
 .../presto/bytecode/DumpBytecodeVisitor.java       | 601 +++++++++++++++
 .../presto/bytecode/DynamicClassLoader.java        | 144 ++++
 .../facebook/presto/bytecode/FieldDefinition.java  | 106 +++
 .../facebook/presto/bytecode/MethodDefinition.java | 284 +++++++
 .../presto/bytecode/MethodGenerationContext.java   | 122 +++
 .../java/com/facebook/presto/bytecode/OpCode.java  | 268 +++++++
 .../com/facebook/presto/bytecode/Parameter.java    |  33 +
 .../presto/bytecode/ParameterizedType.java         | 285 +++++++
 .../java/com/facebook/presto/bytecode/Scope.java   | 102 +++
 .../facebook/presto/bytecode/SmartClassWriter.java |  54 ++
 .../com/facebook/presto/bytecode/Variable.java     |  89 +++
 .../presto/bytecode/control/CaseStatement.java     |  72 ++
 .../presto/bytecode/control/DoWhileLoop.java       | 106 +++
 .../presto/bytecode/control/FlowControl.java       |  22 +
 .../facebook/presto/bytecode/control/ForLoop.java  | 155 ++++
 .../presto/bytecode/control/IfStatement.java       | 124 +++
 .../presto/bytecode/control/SwitchStatement.java   | 171 +++++
 .../facebook/presto/bytecode/control/TryCatch.java |  97 +++
 .../presto/bytecode/control/WhileLoop.java         | 100 +++
 .../facebook/presto/bytecode/debug/DebugNode.java  |  21 +
 .../presto/bytecode/debug/LineNumberNode.java      |  59 ++
 .../presto/bytecode/debug/LocalVariableNode.java   |  62 ++
 .../bytecode/expression/AndBytecodeExpression.java |  64 ++
 .../expression/ArithmeticBytecodeExpression.java   | 202 +++++
 .../expression/ArrayLengthBytecodeExpression.java  |  55 ++
 .../bytecode/expression/BytecodeExpression.java    | 221 ++++++
 .../bytecode/expression/BytecodeExpressions.java   | 623 +++++++++++++++
 .../expression/CastBytecodeExpression.java         | 328 ++++++++
 .../expression/ComparisonBytecodeExpression.java   | 313 ++++++++
 .../expression/ConstantBytecodeExpression.java     |  68 ++
 .../expression/GetElementBytecodeExpression.java   |  63 ++
 .../expression/GetFieldBytecodeExpression.java     | 109 +++
 .../expression/InlineIfBytecodeExpression.java     |  69 ++
 .../expression/InstanceOfBytecodeExpression.java   |  62 ++
 .../expression/InvokeBytecodeExpression.java       | 115 +++
 .../InvokeDynamicBytecodeExpression.java           |  84 +++
 .../expression/NegateBytecodeExpression.java       |  66 ++
 .../expression/NewArrayBytecodeExpression.java     |  99 +++
 .../expression/NewInstanceBytecodeExpression.java  |  67 ++
 .../bytecode/expression/NotBytecodeExpression.java |  63 ++
 .../bytecode/expression/OrBytecodeExpression.java  |  64 ++
 .../bytecode/expression/PopBytecodeExpression.java |  49 ++
 .../expression/ReturnBytecodeExpression.java       |  82 ++
 .../SetArrayElementBytecodeExpression.java         |  68 ++
 .../expression/SetFieldBytecodeExpression.java     | 129 ++++
 .../presto/bytecode/instruction/Constant.java      | 542 +++++++++++++
 .../bytecode/instruction/FieldInstruction.java     | 156 ++++
 .../bytecode/instruction/InstructionNode.java      |  21 +
 .../bytecode/instruction/InvokeInstruction.java    | 405 ++++++++++
 .../bytecode/instruction/JumpInstruction.java      | 137 ++++
 .../presto/bytecode/instruction/LabelNode.java     |  70 ++
 .../bytecode/instruction/TypeInstruction.java      | 109 +++
 .../bytecode/instruction/VariableInstruction.java  | 126 ++++
 .../presto/bytecode/TestBytecodeUtils.java         |  29 +
 .../presto/bytecode/TestClassGenerator.java        |  97 +++
 .../expression/BytecodeExpressionAssertions.java   | 148 ++++
 .../TestArithmeticBytecodeExpression.java          | 136 ++++
 .../expression/TestArrayBytecodeExpressions.java   | 155 ++++
 .../expression/TestCastBytecodeExpression.java     | 164 ++++
 .../TestComparisonBytecodeExpression.java          | 179 +++++
 .../expression/TestConstantBytecodeExpression.java |  75 ++
 .../expression/TestGetFieldBytecodeExpression.java |  41 +
 .../expression/TestInlineIfBytecodeExpression.java |  32 +
 .../expression/TestInvokeBytecodeExpression.java   |  55 ++
 .../TestInvokeDynamicBytecodeExpression.java       |  56 ++
 .../expression/TestLogicalBytecodeExpression.java  |  51 ++
 .../TestNewInstanceBytecodeExpression.java         |  34 +
 .../expression/TestPopBytecodeExpression.java      |  46 ++
 .../expression/TestSetFieldBytecodeExpression.java |  85 +++
 .../TestSetVariableBytecodeExpression.java         |  47 ++
 modules/cli-common/pom.xml                         |   1 +
 modules/schema/README.md                           |  49 ++
 modules/schema/pom.xml                             |  84 +++
 .../ignite/internal/schema/AssemblyException.java  |  32 +
 .../org/apache/ignite/internal/schema/Bitmask.java |  87 +++
 .../ignite/internal/schema/ByteBufferTuple.java    |  91 +++
 .../org/apache/ignite/internal/schema/Column.java  | 137 ++++
 .../org/apache/ignite/internal/schema/Columns.java | 279 +++++++
 .../ignite/internal/schema/ExpandableByteBuf.java  | 253 +++++++
 .../internal/schema/InvalidTypeException.java      |  30 +
 .../apache/ignite/internal/schema/NativeType.java  | 142 ++++
 .../ignite/internal/schema/NativeTypeSpec.java     | 178 +++++
 .../org/apache/ignite/internal/schema/README.md    |  87 +++
 .../ignite/internal/schema/SchemaDescriptor.java   |  87 +++
 .../org/apache/ignite/internal/schema/Tuple.java   | 429 +++++++++++
 .../ignite/internal/schema/TupleAssembler.java     | 453 +++++++++++
 .../schema/marshaller/AbstractSerializer.java      | 108 +++
 .../internal/schema/marshaller/BinaryMode.java     |  90 +++
 .../internal/schema/marshaller/MarshallerUtil.java | 115 +++
 .../schema/marshaller/SerializationException.java  |  42 ++
 .../internal/schema/marshaller/Serializer.java     |  49 ++
 .../schema/marshaller/SerializerFactory.java       |  52 ++
 .../marshaller/asm/AsmSerializerGenerator.java     | 458 +++++++++++
 .../asm/IdentityMarshallerCodeGenerator.java       |  77 ++
 .../marshaller/asm/MarshallerCodeGenerator.java    |  71 ++
 .../asm/ObjectMarshallerCodeGenerator.java         | 182 +++++
 .../asm/TupleColumnAccessCodeGenerator.java        | 139 ++++
 .../marshaller/reflection/FieldAccessor.java       | 643 ++++++++++++++++
 .../marshaller/reflection/JavaSerializer.java      | 157 ++++
 .../reflection/JavaSerializerFactory.java          |  32 +
 .../schema/marshaller/reflection/Marshaller.java   | 153 ++++
 .../ignite/internal/schema/package-info.java       |  22 +
 .../org/apache/ignite/internal/util/Factory.java   |  32 +
 .../apache/ignite/internal/util/ObjectFactory.java |  54 ++
 .../java/org/apache/ignite/internal/util/Pair.java |  57 ++
 .../benchmarks/SerializerBenchmarkTest.java        | 206 +++++
 .../apache/ignite/internal/schema/ColumnTest.java  |  48 ++
 .../apache/ignite/internal/schema/ColumnsTest.java | 443 +++++++++++
 .../internal/schema/ExpandableByteBufTest.java     | 150 ++++
 .../ignite/internal/schema/NativeTypeTest.java     |  71 ++
 .../internal/schema/SchemaDescriptorTest.java      |  51 ++
 .../apache/ignite/internal/schema/TestUtils.java   | 127 ++++
 .../apache/ignite/internal/schema/TupleTest.java   | 320 ++++++++
 .../schema/marshaller/JavaSerializerTest.java      | 675 +++++++++++++++++
 .../marshaller/reflection/FieldAccessorTest.java   | 417 ++++++++++
 parent/pom.xml                                     |  42 ++
 pom.xml                                            |   2 +
 136 files changed, 19485 insertions(+), 31 deletions(-)

diff --git a/check-rules/pmd-rules.xml b/check-rules/pmd-rules.xml
index 7cee12f..3d2e458 100644
--- a/check-rules/pmd-rules.xml
+++ b/check-rules/pmd-rules.xml
@@ -20,21 +20,27 @@
 <ruleset name="Default Maven PMD Plugin Ruleset"
          xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">
+         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.io/ruleset_2_0_0.xsd">
 
     <description>
         PMD Ruleset for Apache Ignite
     </description>
 
-    <rule ref="category/java/errorprone.xml/EmptyFinallyBlock" />
-    <rule ref="category/java/errorprone.xml/AvoidCatchingNPE" />
-    <rule ref="category/java/errorprone.xml/BrokenNullCheck" />
-    <rule ref="category/java/errorprone.xml/EmptySwitchStatements" />
-    <rule ref="category/java/errorprone.xml/EmptyStatementNotInLoop" />
-    <rule ref="category/java/errorprone.xml/EmptySynchronizedBlock" />
-    <rule ref="category/java/errorprone.xml/UselessOperationOnImmutable" />
-
-    <rule ref="category/java/bestpractices.xml/UnusedLocalVariable" />
-    <rule ref="category/java/bestpractices.xml/UnusedPrivateField" />
-    <rule ref="category/java/bestpractices.xml/UnusedPrivateMethod" />
+    <rule ref="category/java/errorprone.xml/EmptyFinallyBlock"/>
+    <rule ref="category/java/errorprone.xml/AvoidCatchingNPE"/>
+    <rule ref="category/java/errorprone.xml/BrokenNullCheck"/>
+    <rule ref="category/java/errorprone.xml/EmptySwitchStatements"/>
+    <rule ref="category/java/errorprone.xml/EmptyStatementNotInLoop"/>
+    <rule ref="category/java/errorprone.xml/EmptySynchronizedBlock"/>
+    <rule ref="category/java/errorprone.xml/UselessOperationOnImmutable"/>
+
+    <rule ref="category/java/bestpractices.xml/UnusedLocalVariable"/>
+    <rule ref="category/java/bestpractices.xml/UnusedPrivateField"/>
+    <!--
+        UnusedPrivateMethod has a known bug which leads to false positive triggering if
+        method signature contains generic parameter and\or descendant class is passed as parameter.
+        See for details: https://github.com/pmd/pmd/issues/770
+    -->
+    <!--    <rule ref="category/java/bestpractices.xml/UnusedPrivateMethod" />-->
+
 </ruleset>
diff --git a/modules/README.md b/modules/README.md
index 6b3f13b..091b7f0 100644
--- a/modules/README.md
+++ b/modules/README.md
@@ -10,10 +10,12 @@ We prohibit cyclic dependencies between modules in order to simplify JIGSAW migr
 
 Module Name | Description
 ----------- | -----------
-[network](network/README.md)|Networking module: group membership and message passing
-[configuration-annotation-processor](configuration-annotation-processor/README.md)|Tooling for generating Ignite configuration model classes from configuration schema definition
+[bytecode](bytecode/README.md)|Ignite Bytecode module.
+[cli](cli/README.md)|Ignite CLI implementation
+[cli-common](cli-common/README.md)|Shared interfaces definitions for pluggable CLIng
 [configuration](configuration/README.md)|Ignite configuration classes and configuration management framework
-[runner](runner/README.md)|Ignite server node runner. The module that wires up the Ignite components and handles node lifecycle
+[configuration-annotation-processor](configuration-annotation-processor/README.md)|Tooling for generating Ignite configuration model classes from configuration schema definition
+[network](network/README.md)|Networking module: group membership and message passi
 [rest](rest/README.md)|REST management endpoint bindings and command handlers
-[cli-common](cli-common/README.md)|Shared interfaces definitions for pluggable CLI
-[cli](cli/README.md)|Ignite CLI implementation
+[runner](runner/README.md)|Ignite server node runner. The module that wires up the Ignite components and handles node lifecycle
+[schema](schema/README.md)|Ignite schema API implementation and schema management classes.
\ No newline at end of file
diff --git a/modules/bytecode/README.md b/modules/bytecode/README.md
new file mode 100644
index 0000000..3a178c8
--- /dev/null
+++ b/modules/bytecode/README.md
@@ -0,0 +1,6 @@
+# Apache Ignite Bytecode module
+Fork of [PrestoDB Bytecode module (ver 0.243)](https://github.com/prestodb/presto/tree/0.243/presto-bytecode).
+* Removed unnecessary guava dependency.
+* Tests migrated from TestNG to JUnit 5.
+
+This module provides a convenient thin wrapper around [ASM](https://asm.ow2.io/) library to generate classes at runtime.
\ No newline at end of file
diff --git a/pom.xml b/modules/bytecode/pom.xml
similarity index 58%
copy from pom.xml
copy to modules/bytecode/pom.xml
index 86a51b8..d475a49 100644
--- a/pom.xml
+++ b/modules/bytecode/pom.xml
@@ -26,20 +26,38 @@
         <groupId>org.apache.ignite</groupId>
         <artifactId>ignite-parent</artifactId>
         <version>1</version>
-        <relativePath>parent</relativePath>
+        <relativePath>../../parent/pom.xml</relativePath>
     </parent>
 
-    <artifactId>apache-ignite</artifactId>
+    <artifactId>ignite-bytecode</artifactId>
     <version>3.0.0-SNAPSHOT</version>
-    <packaging>pom</packaging>
-
-    <modules>
-        <module>modules/cli</module>
-        <module>modules/cli-common</module>
-        <module>modules/configuration</module>
-        <module>modules/configuration-annotation-processor</module>
-        <module>modules/rest</module>
-        <module>modules/runner</module>
-        <module>modules/network</module>
-    </modules>
-</project>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ow2.asm</groupId>
+            <artifactId>asm</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ow2.asm</groupId>
+            <artifactId>asm-tree</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.ow2.asm</groupId>
+            <artifactId>asm-util</artifactId>
+        </dependency>
+
+        <!-- Test dependencies -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
\ No newline at end of file
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Access.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Access.java
new file mode 100644
index 0000000..1b531c5
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Access.java
@@ -0,0 +1,85 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.EnumSet;
+import java.util.List;
+
+import static java.util.Locale.ENGLISH;
+import static org.objectweb.asm.Opcodes.ACC_ABSTRACT;
+import static org.objectweb.asm.Opcodes.ACC_ANNOTATION;
+import static org.objectweb.asm.Opcodes.ACC_BRIDGE;
+import static org.objectweb.asm.Opcodes.ACC_ENUM;
+import static org.objectweb.asm.Opcodes.ACC_FINAL;
+import static org.objectweb.asm.Opcodes.ACC_INTERFACE;
+import static org.objectweb.asm.Opcodes.ACC_NATIVE;
+import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
+import static org.objectweb.asm.Opcodes.ACC_PROTECTED;
+import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
+import static org.objectweb.asm.Opcodes.ACC_STATIC;
+import static org.objectweb.asm.Opcodes.ACC_STRICT;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.ACC_SYNCHRONIZED;
+import static org.objectweb.asm.Opcodes.ACC_SYNTHETIC;
+import static org.objectweb.asm.Opcodes.ACC_TRANSIENT;
+import static org.objectweb.asm.Opcodes.ACC_VARARGS;
+import static org.objectweb.asm.Opcodes.ACC_VOLATILE;
+
+public enum Access {
+    PUBLIC(ACC_PUBLIC),
+    PRIVATE(ACC_PRIVATE),
+    PROTECTED(ACC_PROTECTED),
+    STATIC(ACC_STATIC),
+    FINAL(ACC_FINAL),
+    SUPER(ACC_SUPER),
+    SYNCHRONIZED(ACC_SYNCHRONIZED),
+    VOLATILE(ACC_VOLATILE),
+    BRIDGE(ACC_BRIDGE),
+    VARARGS(ACC_VARARGS),
+    TRANSIENT(ACC_TRANSIENT),
+    NATIVE(ACC_NATIVE),
+    INTERFACE(ACC_INTERFACE),
+    ABSTRACT(ACC_ABSTRACT),
+    STRICT(ACC_STRICT),
+    SYNTHETIC(ACC_SYNTHETIC),
+    ANNOTATION(ACC_ANNOTATION),
+    ENUM(ACC_ENUM);
+
+    private final int modifier;
+
+    Access(int modifier) {
+        this.modifier = modifier;
+    }
+
+    public int getModifier() {
+        return modifier;
+    }
+
+    @Override
+    public String toString() {
+        return name().toLowerCase(ENGLISH);
+    }
+
+    public static EnumSet<Access> a(Access... access) {
+        return EnumSet.copyOf(List.of(access));
+    }
+
+    public static int toAccessModifier(Iterable<Access> accesses) {
+        int modifier = 0;
+        for (Access access : accesses) {
+            modifier += access.getModifier();
+        }
+        return modifier;
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AddFakeLineNumberClassVisitor.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AddFakeLineNumberClassVisitor.java
new file mode 100644
index 0000000..5d79270
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AddFakeLineNumberClassVisitor.java
@@ -0,0 +1,157 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.Handle;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+import static org.objectweb.asm.Opcodes.ASM5;
+
+class AddFakeLineNumberClassVisitor
+        extends ClassVisitor
+{
+    int methodCount;
+
+    AddFakeLineNumberClassVisitor(ClassVisitor cv)
+    {
+        super(ASM5, cv);
+        super.visitSource("FakeSource.java", null);
+    }
+
+    @Override
+    public void visitSource(String source, String debug)
+    {
+        super.visitSource(source, debug);
+    }
+
+    @Override
+    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
+    {
+        MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
+        methodCount++;
+        return new AddFakeLineNumberMethodVisitor(methodVisitor, 1000 * methodCount);
+    }
+
+    private static class AddFakeLineNumberMethodVisitor
+            extends MethodVisitor
+    {
+        private int count;
+
+        AddFakeLineNumberMethodVisitor(MethodVisitor mv, int startLineNumber)
+        {
+            super(ASM5, mv);
+            this.count = startLineNumber;
+        }
+
+        private void addFakeLineNumber()
+        {
+            Label label = new Label();
+            mv.visitLabel(label);
+            mv.visitLineNumber(++count, label);
+        }
+
+        @Override
+        public void visitInsn(int opcode)
+        {
+            addFakeLineNumber();
+            super.visitInsn(opcode);
+        }
+
+        @Override
+        public void visitIntInsn(int opcode, int operand)
+        {
+            addFakeLineNumber();
+            super.visitIntInsn(opcode, operand);
+        }
+
+        @Override
+        public void visitVarInsn(int opcode, int var)
+        {
+            addFakeLineNumber();
+            super.visitVarInsn(opcode, var);
+        }
+
+        @Override
+        public void visitTypeInsn(int opcode, String type)
+        {
+            addFakeLineNumber();
+            super.visitTypeInsn(opcode, type);
+        }
+
+        @Override
+        public void visitFieldInsn(int opcode, String owner, String name, String desc)
+        {
+            addFakeLineNumber();
+            super.visitFieldInsn(opcode, owner, name, desc);
+        }
+
+        @Override
+        public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf)
+        {
+            addFakeLineNumber();
+            super.visitMethodInsn(opcode, owner, name, desc, itf);
+        }
+
+        @Override
+        public void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs)
+        {
+            addFakeLineNumber();
+            super.visitInvokeDynamicInsn(name, desc, bsm, bsmArgs);
+        }
+
+        @Override
+        public void visitJumpInsn(int opcode, Label label)
+        {
+            addFakeLineNumber();
+            super.visitJumpInsn(opcode, label);
+        }
+
+        @Override
+        public void visitLdcInsn(Object cst)
+        {
+            addFakeLineNumber();
+            super.visitLdcInsn(cst);
+        }
+
+        @Override
+        public void visitIincInsn(int var, int increment)
+        {
+            addFakeLineNumber();
+            super.visitIincInsn(var, increment);
+        }
+
+        @Override
+        public void visitTableSwitchInsn(int min, int max, Label dflt, Label... labels)
+        {
+            addFakeLineNumber();
+            super.visitTableSwitchInsn(min, max, dflt, labels);
+        }
+
+        @Override
+        public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels)
+        {
+            addFakeLineNumber();
+            super.visitLookupSwitchInsn(dflt, keys, labels);
+        }
+
+        @Override
+        public void visitMultiANewArrayInsn(String desc, int dims)
+        {
+            addFakeLineNumber();
+            super.visitMultiANewArrayInsn(desc, dims);
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java
new file mode 100644
index 0000000..6a1d41d
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/AnnotationDefinition.java
@@ -0,0 +1,211 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+public class AnnotationDefinition {
+    private static final Set<Class<?>> ALLOWED_TYPES = Set.of(
+        Byte.class,
+        Character.class,
+        Double.class,
+        Float.class,
+        Integer.class,
+        Long.class,
+        Short.class,
+        Void.class,
+        String.class,
+        Class.class,
+        ParameterizedType.class,
+        AnnotationDefinition.class,
+        Enum.class);
+
+    private final ParameterizedType type;
+    private final Map<String, Object> values = new LinkedHashMap<>();
+
+    public AnnotationDefinition(Class<?> type) {
+        this.type = type(type);
+    }
+
+    public AnnotationDefinition(ParameterizedType type) {
+        this.type = type;
+    }
+
+    public AnnotationDefinition setValue(String name, Byte value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Boolean value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Character value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Short value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Integer value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Long value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Float value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Double value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, String value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Class<?> value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, ParameterizedType value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, AnnotationDefinition value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, Enum<?> value) {
+        return setValueInternal(name, value);
+    }
+
+    public AnnotationDefinition setValue(String name, List<?> value) {
+        return setValueInternal(name, value);
+    }
+
+    private AnnotationDefinition setValueInternal(String name, Object value) {
+        requireNonNull(name, "name is null");
+        requireNonNull(value, "value is null");
+
+        isValidType(value);
+
+        values.put(name, value);
+        return this;
+    }
+
+    public ParameterizedType getType() {
+        return type;
+    }
+
+    public Map<String, Object> getValues() {
+        // todo we need an unmodifiable view
+        return values;
+    }
+
+    @SuppressWarnings("OverlyStrongTypeCast")
+    private static void isValidType(Object value) {
+        if (value instanceof List) {
+            // todo verify list contains single type
+            for (Object v : (List<Object>)value) {
+                checkArgument(ALLOWED_TYPES.contains(v.getClass()), "List contains invalid type %s", v.getClass());
+                if (v instanceof List) {
+                    isValidType(value);
+                }
+            }
+        }
+        else {
+            checkArgument(ALLOWED_TYPES.contains(value.getClass()), "Invalid value type %s", value.getClass());
+        }
+    }
+
+    public void visitClassAnnotation(ClassVisitor visitor) {
+        AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true);
+        visit(annotationVisitor);
+        annotationVisitor.visitEnd();
+    }
+
+    public void visitFieldAnnotation(FieldVisitor visitor) {
+        AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true);
+        visit(annotationVisitor);
+        annotationVisitor.visitEnd();
+    }
+
+    public void visitMethodAnnotation(MethodVisitor visitor) {
+        AnnotationVisitor annotationVisitor = visitor.visitAnnotation(type.getType(), true);
+        visit(annotationVisitor);
+        annotationVisitor.visitEnd();
+    }
+
+    public void visitParameterAnnotation(int parameterIndex, MethodVisitor visitor) {
+        AnnotationVisitor annotationVisitor = visitor.visitParameterAnnotation(parameterIndex, type.getType(), true);
+        visit(annotationVisitor);
+        annotationVisitor.visitEnd();
+    }
+
+    private void visit(AnnotationVisitor visitor) {
+        for (Entry<String, Object> entry : values.entrySet()) {
+            String name = entry.getKey();
+            Object value = entry.getValue();
+            visit(visitor, name, value);
+        }
+    }
+
+    @SuppressWarnings("OverlyStrongTypeCast")
+    private static void visit(AnnotationVisitor visitor, String name, Object value) {
+        if (value instanceof AnnotationDefinition) {
+            AnnotationDefinition annotation = (AnnotationDefinition)value;
+            AnnotationVisitor annotationVisitor = visitor.visitAnnotation(name, annotation.type.getType());
+            annotation.visit(annotationVisitor);
+        }
+        else if (value instanceof Enum) {
+            Enum<?> enumConstant = (Enum<?>)value;
+            visitor.visitEnum(name, type(enumConstant.getDeclaringClass()).getClassName(), enumConstant.name());
+        }
+        else if (value instanceof ParameterizedType) {
+            ParameterizedType parameterizedType = (ParameterizedType)value;
+            visitor.visit(name, Type.getType(parameterizedType.getType()));
+        }
+        else if (value instanceof Class) {
+            Class<?> clazz = (Class<?>)value;
+            visitor.visit(name, Type.getType(clazz));
+        }
+        else if (value instanceof List) {
+            AnnotationVisitor arrayVisitor = visitor.visitArray(name);
+            for (Object element : (List<?>)value) {
+                visit(arrayVisitor, null, element);
+            }
+            arrayVisitor.visitEnd();
+        }
+        else {
+            visitor.visit(name, value);
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ArrayOpCode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ArrayOpCode.java
new file mode 100644
index 0000000..b11f831
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ArrayOpCode.java
@@ -0,0 +1,124 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_BOOLEAN;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_BYTE;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_CHAR;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_DOUBLE;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_FLOAT;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_INT;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_LONG;
+import static com.facebook.presto.bytecode.ArrayOpCode.AType.T_SHORT;
+import static com.facebook.presto.bytecode.OpCode.AALOAD;
+import static com.facebook.presto.bytecode.OpCode.AASTORE;
+import static com.facebook.presto.bytecode.OpCode.BALOAD;
+import static com.facebook.presto.bytecode.OpCode.BASTORE;
+import static com.facebook.presto.bytecode.OpCode.CALOAD;
+import static com.facebook.presto.bytecode.OpCode.CASTORE;
+import static com.facebook.presto.bytecode.OpCode.DALOAD;
+import static com.facebook.presto.bytecode.OpCode.DASTORE;
+import static com.facebook.presto.bytecode.OpCode.FALOAD;
+import static com.facebook.presto.bytecode.OpCode.FASTORE;
+import static com.facebook.presto.bytecode.OpCode.IALOAD;
+import static com.facebook.presto.bytecode.OpCode.IASTORE;
+import static com.facebook.presto.bytecode.OpCode.LALOAD;
+import static com.facebook.presto.bytecode.OpCode.LASTORE;
+import static com.facebook.presto.bytecode.OpCode.SALOAD;
+import static com.facebook.presto.bytecode.OpCode.SASTORE;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+
+public enum ArrayOpCode {
+    NOT_PRIMITIVE(null, AALOAD, AASTORE, null),
+    BYTE(byte.class, BALOAD, BASTORE, T_BYTE),
+    BOOLEAN(boolean.class, BALOAD, BASTORE, T_BOOLEAN),
+    CHAR(char.class, CALOAD, CASTORE, T_CHAR),
+    SHORT(short.class, SALOAD, SASTORE, T_SHORT),
+    INT(int.class, IALOAD, IASTORE, T_INT),
+    LONG(long.class, LALOAD, LASTORE, T_LONG),
+    FLOAT(float.class, FALOAD, FASTORE, T_FLOAT),
+    DOUBLE(double.class, DALOAD, DASTORE, T_DOUBLE);
+
+    private final OpCode load;
+    private final OpCode store;
+    private final AType atype;
+    private final Class<?> type;
+
+    private static final Map<Class<?>, ArrayOpCode> arrayOpCodeMap = initializeArrayOpCodeMap();
+
+    ArrayOpCode(@Nullable Class<?> clazz, OpCode load, OpCode store, @Nullable AType atype) {
+        this.type = clazz;
+        this.load = requireNonNull(load, "load is null");
+        this.store = requireNonNull(store, "store is null");
+        this.atype = atype;
+    }
+
+    public OpCode getLoad() {
+        return load;
+    }
+
+    public OpCode getStore() {
+        return store;
+    }
+
+    public int getAtype() {
+        return atype.getType();
+    }
+
+    public Class<?> getType() {
+        return type;
+    }
+
+    public static ArrayOpCode getArrayOpCode(ParameterizedType type) {
+        if (!type.isPrimitive()) {
+            return NOT_PRIMITIVE;
+        }
+        ArrayOpCode arrayOpCode = arrayOpCodeMap.get(type.getPrimitiveType());
+        if (arrayOpCode == null) {
+            throw new IllegalArgumentException("unsupported primitive type " + type);
+        }
+        return arrayOpCode;
+    }
+
+    static Map<Class<?>, ArrayOpCode> initializeArrayOpCodeMap() {
+        return stream(values()).filter(o -> o.getType() != null).collect(Collectors.toMap(ArrayOpCode::getType, Function.identity()));
+    }
+
+    enum AType {
+        T_BOOLEAN(4),
+        T_CHAR(5),
+        T_FLOAT(6),
+        T_DOUBLE(7),
+        T_BYTE(8),
+        T_SHORT(9),
+        T_INT(10),
+        T_LONG(11);
+
+        private final int type;
+
+        AType(int type) {
+            this.type = type;
+        }
+
+        int getType() {
+            return type;
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ByteCodeTooLargeException.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ByteCodeTooLargeException.java
new file mode 100644
index 0000000..49c7285
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ByteCodeTooLargeException.java
@@ -0,0 +1,23 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+public class ByteCodeTooLargeException
+        extends RuntimeException
+{
+    public ByteCodeTooLargeException(Exception cause)
+    {
+        super(cause);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeBlock.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeBlock.java
new file mode 100644
index 0000000..06d2b5b
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeBlock.java
@@ -0,0 +1,837 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.debug.LineNumberNode;
+import com.facebook.presto.bytecode.instruction.Constant;
+import com.facebook.presto.bytecode.instruction.InvokeInstruction;
+import com.facebook.presto.bytecode.instruction.JumpInstruction;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import com.facebook.presto.bytecode.instruction.TypeInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction;
+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.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.Access.STATIC;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.instruction.Constant.loadBoolean;
+import static com.facebook.presto.bytecode.instruction.Constant.loadClass;
+import static com.facebook.presto.bytecode.instruction.Constant.loadDouble;
+import static com.facebook.presto.bytecode.instruction.Constant.loadFloat;
+import static com.facebook.presto.bytecode.instruction.Constant.loadInt;
+import static com.facebook.presto.bytecode.instruction.Constant.loadLong;
+import static com.facebook.presto.bytecode.instruction.Constant.loadNumber;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.getFieldInstruction;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.getStaticInstruction;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.putFieldInstruction;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.putStaticInstruction;
+import static com.facebook.presto.bytecode.instruction.TypeInstruction.cast;
+import static com.facebook.presto.bytecode.instruction.TypeInstruction.instanceOf;
+import static com.facebook.presto.bytecode.instruction.VariableInstruction.loadVariable;
+import static com.facebook.presto.bytecode.instruction.VariableInstruction.storeVariable;
+
+@SuppressWarnings("UnusedDeclaration")
+public class BytecodeBlock
+    implements BytecodeNode {
+    private final List<BytecodeNode> nodes = new ArrayList<>();
+
+    private String description;
+    private int currentLineNumber = -1;
+
+    public String getDescription() {
+        return description;
+    }
+
+    public BytecodeBlock setDescription(String description) {
+        this.description = description;
+        return this;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.copyOf(nodes);
+    }
+
+    public BytecodeBlock append(BytecodeNode node) {
+        nodes.add(node);
+        return this;
+    }
+
+    public BytecodeBlock comment(String comment) {
+        nodes.add(new Comment(comment));
+        return this;
+    }
+
+    public BytecodeBlock comment(String comment, Object... args) {
+        nodes.add(new Comment(String.format(comment, args)));
+        return this;
+    }
+
+    public boolean isEmpty() {
+        return nodes.isEmpty();
+    }
+
+    public BytecodeBlock visitLabel(LabelNode label) {
+        nodes.add(label);
+        return this;
+    }
+
+    public BytecodeBlock gotoLabel(LabelNode label) {
+        nodes.add(JumpInstruction.jump(label));
+        return this;
+    }
+
+    public BytecodeBlock ifFalseGoto(LabelNode label) {
+        return ifZeroGoto(label);
+    }
+
+    public BytecodeBlock ifTrueGoto(LabelNode label) {
+        return ifNotZeroGoto(label);
+    }
+
+    public BytecodeBlock ifZeroGoto(LabelNode label) {
+        nodes.add(JumpInstruction.jumpIfEqualZero(label));
+        return this;
+    }
+
+    public BytecodeBlock ifNotZeroGoto(LabelNode label) {
+        nodes.add(JumpInstruction.jumpIfNotEqualZero(label));
+        return this;
+    }
+
+    public BytecodeBlock ifNullGoto(LabelNode label) {
+        nodes.add(JumpInstruction.jumpIfNull(label));
+        return this;
+    }
+
+    public BytecodeBlock ifNotNullGoto(LabelNode label) {
+        nodes.add(JumpInstruction.jumpIfNotNull(label));
+        return this;
+    }
+
+    public BytecodeBlock intAdd() {
+        nodes.add(OpCode.IADD);
+        return this;
+    }
+
+    public BytecodeBlock longAdd() {
+        nodes.add(OpCode.LADD);
+        return this;
+    }
+
+    public BytecodeBlock longCompare() {
+        nodes.add(OpCode.LCMP);
+        return this;
+    }
+
+    /**
+     * Compare two doubles. If either is NaN comparison is -1.
+     */
+    public BytecodeBlock doubleCompareNanLess() {
+        nodes.add(OpCode.DCMPL);
+        return this;
+    }
+
+    /**
+     * Compare two doubles. If either is NaN comparison is 1.
+     */
+    public BytecodeBlock doubleCompareNanGreater() {
+        nodes.add(OpCode.DCMPG);
+        return this;
+    }
+
+    public BytecodeBlock intLeftShift() {
+        nodes.add(OpCode.ISHL);
+        return this;
+    }
+
+    public BytecodeBlock intRightShift() {
+        nodes.add(OpCode.ISHR);
+        return this;
+    }
+
+    public BytecodeBlock longLeftShift() {
+        nodes.add(OpCode.LSHL);
+        return this;
+    }
+
+    public BytecodeBlock longRightShift() {
+        nodes.add(OpCode.LSHR);
+        return this;
+    }
+
+    public BytecodeBlock unsignedIntRightShift() {
+        nodes.add(OpCode.IUSHR);
+        return this;
+    }
+
+    public BytecodeBlock unsignedLongRightShift() {
+        nodes.add(OpCode.LUSHR);
+        return this;
+    }
+
+    public BytecodeBlock intBitAnd() {
+        nodes.add(OpCode.IAND);
+        return this;
+    }
+
+    public BytecodeBlock intBitOr() {
+        nodes.add(OpCode.IOR);
+        return this;
+    }
+
+    public BytecodeBlock intBitXor() {
+        nodes.add(OpCode.IXOR);
+        return this;
+    }
+
+    public BytecodeBlock longBitAnd() {
+        nodes.add(OpCode.LAND);
+        return this;
+    }
+
+    public BytecodeBlock longBitOr() {
+        nodes.add(OpCode.LOR);
+        return this;
+    }
+
+    public BytecodeBlock longBitXor() {
+        nodes.add(OpCode.LXOR);
+        return this;
+    }
+
+    public BytecodeBlock intNegate() {
+        nodes.add(OpCode.INEG);
+        return this;
+    }
+
+    public BytecodeBlock intToLong() {
+        nodes.add(OpCode.I2L);
+        return this;
+    }
+
+    public BytecodeBlock longNegate() {
+        nodes.add(OpCode.LNEG);
+        return this;
+    }
+
+    public BytecodeBlock longToInt() {
+        nodes.add(OpCode.L2I);
+        return this;
+    }
+
+    public BytecodeBlock isInstanceOf(Class<?> type) {
+        nodes.add(instanceOf(type));
+        return this;
+    }
+
+    public BytecodeBlock isInstanceOf(ParameterizedType type) {
+        nodes.add(instanceOf(type));
+        return this;
+    }
+
+    public BytecodeBlock checkCast(Class<?> type) {
+        nodes.add(cast(type));
+        return this;
+    }
+
+    public BytecodeBlock checkCast(ParameterizedType type) {
+        nodes.add(cast(type));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(Method method) {
+        nodes.add(InvokeInstruction.invokeStatic(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(MethodDefinition method) {
+        nodes.add(InvokeInstruction.invokeStatic(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(Class<?> type, String name, Class<?> returnType, Class<?>... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(Class<?> type, String name, Class<?> returnType,
+        Collection<Class<?>> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(ParameterizedType type, String name, ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeStatic(ParameterizedType type, String name, ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeStatic(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(Method method) {
+        nodes.add(InvokeInstruction.invokeVirtual(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(MethodDefinition method) {
+        nodes.add(InvokeInstruction.invokeVirtual(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(Class<?> type, String name, Class<?> returnType, Class<?>... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(Class<?> type, String name, Class<?> returnType,
+        Collection<Class<?>> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(ParameterizedType type, String name, ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeVirtual(ParameterizedType type, String name, ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeVirtual(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(Method method) {
+        nodes.add(InvokeInstruction.invokeInterface(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(MethodDefinition method) {
+        nodes.add(InvokeInstruction.invokeInterface(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(Class<?> type, String name, Class<?> returnType, Class<?>... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(Class<?> type, String name, Class<?> returnType,
+        Collection<Class<?>> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(ParameterizedType type, String name, ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeInterface(ParameterizedType type, String name, ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeInterface(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeConstructor(Constructor<?> constructor) {
+        nodes.add(InvokeInstruction.invokeConstructor(constructor));
+        return this;
+    }
+
+    public BytecodeBlock invokeConstructor(Class<?> type, Class<?>... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeConstructor(Class<?> type, Collection<Class<?>> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeConstructor(ParameterizedType type, ParameterizedType... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeConstructor(ParameterizedType type, Collection<ParameterizedType> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeConstructor(type, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(Method method) {
+        nodes.add(InvokeInstruction.invokeSpecial(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(MethodDefinition method) {
+        nodes.add(InvokeInstruction.invokeSpecial(method));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(Class<?> type, String name, Class<?> returnType, Class<?>... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(Class<?> type, String name, Class<?> returnType,
+        Collection<Class<?>> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(ParameterizedType type, String name, ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeSpecial(ParameterizedType type, String name, ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes) {
+        nodes.add(InvokeInstruction.invokeSpecial(type, name, returnType, parameterTypes));
+        return this;
+    }
+
+    public BytecodeBlock invokeDynamic(String name, MethodType methodType, Method bootstrapMethod,
+        Object... defaultBootstrapArguments) {
+        nodes.add(InvokeInstruction.invokeDynamic(name, methodType, bootstrapMethod, defaultBootstrapArguments));
+        return this;
+    }
+
+    public BytecodeNode invokeDynamic(String name,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Method bootstrapMethod,
+        List<Object> bootstrapArgs) {
+        nodes.add(InvokeInstruction.invokeDynamic(name, returnType, parameterTypes, bootstrapMethod, bootstrapArgs));
+        return this;
+    }
+
+    public BytecodeBlock ret(Class<?> type) {
+        if (type == long.class) {
+            retLong();
+        }
+        else if (type == boolean.class) {
+            retBoolean();
+        }
+        else if (type == int.class || type == byte.class || type == char.class || type == short.class) {
+            retInt();
+        }
+        else if (type == float.class) {
+            retFloat();
+        }
+        else if (type == double.class) {
+            retDouble();
+        }
+        else if (type == void.class) {
+            ret();
+        }
+        else if (!type.isPrimitive()) {
+            retObject();
+        }
+        else {
+            throw new IllegalArgumentException("Unsupported type: " + type.getName());
+        }
+
+        return this;
+    }
+
+    public BytecodeBlock ret() {
+        nodes.add(OpCode.RETURN);
+        return this;
+    }
+
+    public BytecodeBlock retObject() {
+        nodes.add(OpCode.ARETURN);
+        return this;
+    }
+
+    public BytecodeBlock retFloat() {
+        nodes.add(OpCode.FRETURN);
+        return this;
+    }
+
+    public BytecodeBlock retDouble() {
+        nodes.add(OpCode.DRETURN);
+        return this;
+    }
+
+    public BytecodeBlock retBoolean() {
+        nodes.add(OpCode.IRETURN);
+        return this;
+    }
+
+    public BytecodeBlock retLong() {
+        nodes.add(OpCode.LRETURN);
+        return this;
+    }
+
+    public BytecodeBlock retInt() {
+        nodes.add(OpCode.IRETURN);
+        return this;
+    }
+
+    public BytecodeBlock throwObject() {
+        nodes.add(OpCode.ATHROW);
+        return this;
+    }
+
+    public BytecodeBlock newObject(Class<?> type) {
+        nodes.add(TypeInstruction.newObject(type));
+        return this;
+    }
+
+    public BytecodeBlock newObject(ParameterizedType type) {
+        nodes.add(TypeInstruction.newObject(type));
+        return this;
+    }
+
+    public BytecodeBlock newArray(Class<?> type) {
+        nodes.add(TypeInstruction.newObjectArray(type));
+        return this;
+    }
+
+    public BytecodeBlock dup() {
+        nodes.add(OpCode.DUP);
+        return this;
+    }
+
+    public BytecodeBlock dup(Class<?> type) {
+        if (type == long.class || type == double.class) {
+            nodes.add(OpCode.DUP2);
+        }
+        else if (type != void.class) {
+            nodes.add(OpCode.DUP);
+        }
+        return this;
+    }
+
+    public BytecodeBlock pop() {
+        nodes.add(OpCode.POP);
+        return this;
+    }
+
+    public BytecodeBlock pop(Class<?> type) {
+        if (type == long.class || type == double.class) {
+            nodes.add(OpCode.POP2);
+        }
+        else if (type != void.class) {
+            nodes.add(OpCode.POP);
+        }
+        return this;
+    }
+
+    public BytecodeBlock pop(ParameterizedType type) {
+        Class<?> primitiveType = type.getPrimitiveType();
+        if (primitiveType == long.class || primitiveType == double.class) {
+            nodes.add(OpCode.POP2);
+        }
+        else if (primitiveType != void.class) {
+            nodes.add(OpCode.POP);
+        }
+        return this;
+    }
+
+    public BytecodeBlock swap() {
+        nodes.add(OpCode.SWAP);
+        return this;
+    }
+
+    //
+    // Fields (non-static)
+    //
+
+    public BytecodeBlock getField(Field field) {
+        return getField(field.getDeclaringClass(), field.getName(), field.getType());
+    }
+
+    public BytecodeBlock getField(FieldDefinition field) {
+        getField(field.getDeclaringClass().getType(), field.getName(), field.getType());
+        return this;
+    }
+
+    public BytecodeBlock getField(Class<?> target, String fieldName, Class<?> fieldType) {
+        getField(type(target), fieldName, type(fieldType));
+        return this;
+    }
+
+    public BytecodeBlock getField(ParameterizedType target, String fieldName, ParameterizedType fieldType) {
+        nodes.add(getFieldInstruction(target, fieldName, fieldType));
+        return this;
+    }
+
+    public BytecodeBlock putField(Field field) {
+        return putField(field.getDeclaringClass(), field.getName(), field.getType());
+    }
+
+    public BytecodeBlock putField(Class<?> target, String fieldName, Class<?> fieldType) {
+        putField(type(target), fieldName, type(fieldType));
+        return this;
+    }
+
+    public BytecodeBlock putField(FieldDefinition field) {
+        checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field);
+        putField(field.getDeclaringClass().getType(), field.getName(), field.getType());
+        return this;
+    }
+
+    public BytecodeBlock putField(ParameterizedType target, String fieldName, ParameterizedType fieldType) {
+        nodes.add(putFieldInstruction(target, fieldName, fieldType));
+        return this;
+    }
+
+    //
+    // Static fields
+    //
+
+    public BytecodeBlock getStaticField(FieldDefinition field) {
+        getStaticField(field.getDeclaringClass().getType(), field.getName(), field.getType());
+        return this;
+    }
+
+    public BytecodeBlock getStaticField(Field field) {
+        checkArgument(Modifier.isStatic(field.getModifiers()), "Field is not static: %s", field);
+        getStaticField(type(field.getDeclaringClass()), field.getName(), type(field.getType()));
+        return this;
+    }
+
+    public BytecodeBlock getStaticField(Class<?> target, String fieldName, Class<?> fieldType) {
+        nodes.add(getStaticInstruction(target, fieldName, fieldType));
+        return this;
+    }
+
+    public BytecodeBlock getStaticField(ParameterizedType target, String fieldName, ParameterizedType fieldType) {
+        nodes.add(getStaticInstruction(target, fieldName, fieldType));
+        return this;
+    }
+
+    public BytecodeBlock getStaticField(ParameterizedType target, FieldDefinition field) {
+        nodes.add(getStaticInstruction(target, field.getName(), field.getType()));
+        return this;
+    }
+
+    public BytecodeBlock putStaticField(FieldDefinition field) {
+        putStaticField(field.getDeclaringClass().getType(), field.getName(), field.getType());
+        return this;
+    }
+
+    public BytecodeBlock putStaticField(ParameterizedType target, FieldDefinition field) {
+        checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field);
+        putStaticField(target, field.getName(), field.getType());
+        return this;
+    }
+
+    public BytecodeBlock putStaticField(ParameterizedType target, String fieldName, ParameterizedType fieldType) {
+        nodes.add(putStaticInstruction(target, fieldName, fieldType));
+        return this;
+    }
+
+    //
+    // Load constants
+    //
+
+    public BytecodeBlock pushNull() {
+        nodes.add(OpCode.ACONST_NULL);
+        return this;
+    }
+
+    public BytecodeBlock push(Class<?> type) {
+        nodes.add(loadClass(type));
+        return this;
+    }
+
+    public BytecodeBlock push(ParameterizedType type) {
+        nodes.add(loadClass(type));
+        return this;
+    }
+
+    public BytecodeBlock push(String value) {
+        nodes.add(Constant.loadString(value));
+        return this;
+    }
+
+    public BytecodeBlock push(Number value) {
+        nodes.add(loadNumber(value));
+        return this;
+    }
+
+    public BytecodeBlock push(int value) {
+        nodes.add(loadInt(value));
+        return this;
+    }
+
+    public BytecodeBlock push(boolean value) {
+        nodes.add(loadBoolean(value));
+        return this;
+    }
+
+    public BytecodeBlock pushJavaDefault(Class<?> type) {
+        if (type == void.class) {
+            return this;
+        }
+        if (type == boolean.class || type == byte.class || type == char.class || type == short.class || type == int.class) {
+            return push(0);
+        }
+        if (type == long.class) {
+            return push(0L);
+        }
+        if (type == float.class) {
+            return push(0.0f);
+        }
+        if (type == double.class) {
+            return push(0.0d);
+        }
+        return pushNull();
+    }
+
+    public BytecodeBlock initializeVariable(Variable variable) {
+        ParameterizedType type = variable.getType();
+        if (type.getType().length() == 1) {
+            switch (type.getType().charAt(0)) {
+                case 'B':
+                case 'Z':
+                case 'S':
+                case 'C':
+                case 'I':
+                    nodes.add(loadInt(0));
+                    break;
+                case 'F':
+                    nodes.add(loadFloat(0));
+                    break;
+                case 'D':
+                    nodes.add(loadDouble(0));
+                    break;
+                case 'J':
+                    nodes.add(loadLong(0));
+                    break;
+                default:
+                    checkArgument(false, "Unknown type '%s'", variable.getType());
+            }
+        }
+        else {
+            nodes.add(Constant.loadNull());
+        }
+
+        nodes.add(storeVariable(variable));
+
+        return this;
+    }
+
+    public BytecodeBlock getVariable(Variable variable) {
+        nodes.add(loadVariable(variable));
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable) {
+        nodes.add(storeVariable(variable));
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, Class<?> type) {
+        nodes.add(loadClass(type));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, ParameterizedType type) {
+        nodes.add(loadClass(type));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, String value) {
+        nodes.add(Constant.loadString(value));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, Number value) {
+        nodes.add(loadNumber(value));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, int value) {
+        nodes.add(loadInt(value));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock putVariable(Variable variable, boolean value) {
+        nodes.add(loadBoolean(value));
+        putVariable(variable);
+        return this;
+    }
+
+    public BytecodeBlock incrementVariable(Variable variable, byte increment) {
+        String type = variable.getType().getClassName();
+        checkArgument(List.of("byte", "short", "int").contains(type), "variable must be an byte, short or int, but is %s", type);
+        nodes.add(VariableInstruction.incrementVariable(variable, increment));
+        return this;
+    }
+
+    public BytecodeBlock getObjectArrayElement() {
+        nodes.add(OpCode.AALOAD);
+        return this;
+    }
+
+    public BytecodeBlock putObjectArrayElement() {
+        nodes.add(OpCode.AASTORE);
+        return this;
+    }
+
+    public BytecodeBlock getIntArrayElement() {
+        nodes.add(OpCode.IALOAD);
+        return this;
+    }
+
+    public BytecodeBlock putIntArrayElement() {
+        nodes.add(OpCode.IASTORE);
+        return this;
+    }
+
+    public BytecodeBlock visitLineNumber(int currentLineNumber) {
+        checkArgument(currentLineNumber >= 0, "currentLineNumber must be positive");
+        if (this.currentLineNumber != currentLineNumber) {
+            nodes.add(new LineNumberNode(currentLineNumber));
+            this.currentLineNumber = currentLineNumber;
+        }
+        return this;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        for (BytecodeNode node : nodes) {
+            node.accept(visitor, generationContext);
+        }
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitBlock(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeNode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeNode.java
new file mode 100644
index 0000000..451d522
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeNode.java
@@ -0,0 +1,26 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+public interface BytecodeNode
+{
+    List<BytecodeNode> getChildNodes();
+
+    void accept(MethodVisitor visitor, MethodGenerationContext generationContext);
+
+    <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor);
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeUtils.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeUtils.java
new file mode 100644
index 0000000..66e6a8a
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeUtils.java
@@ -0,0 +1,118 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.io.StringWriter;
+import java.time.Instant;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import org.jetbrains.annotations.Nullable;
+
+import static java.time.ZoneOffset.UTC;
+
+public final class BytecodeUtils {
+    private static final AtomicLong CLASS_ID = new AtomicLong();
+    private static final DateTimeFormatter TIMESTAMP_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
+
+    private static final Map<Class<?>, Class<?>> PRIMITIVES_TO_WRAPPERS
+        = Map.of(boolean.class, Boolean.class,
+        byte.class, Byte.class,
+        char.class, Character.class,
+        double.class, Double.class,
+        float.class, Float.class,
+        int.class, Integer.class,
+        long.class, Long.class,
+        short.class, Short.class,
+        void.class, Void.class);
+
+    private BytecodeUtils() {
+    }
+
+    public static ParameterizedType makeClassName(String baseName, Optional<String> suffix) {
+        String className = baseName
+            + "_" + suffix.orElseGet(() -> Instant.now().atZone(UTC).format(TIMESTAMP_FORMAT))
+            + "_" + CLASS_ID.incrementAndGet();
+        String javaClassName = toJavaIdentifierString(className);
+        return ParameterizedType.typeFromJavaClassName("com.facebook.presto.$gen." + javaClassName);
+    }
+
+    public static ParameterizedType makeClassName(String baseName) {
+        return makeClassName(baseName, Optional.empty());
+    }
+
+    public static String toJavaIdentifierString(String className) {
+        // replace invalid characters with '_'
+        return className.codePoints().mapToObj(c -> Character.isJavaIdentifierPart(c) ? c : '_' & 0xFFFF)
+            .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString();
+    }
+
+    public static String dumpBytecodeTree(ClassDefinition classDefinition) {
+        StringWriter writer = new StringWriter();
+        new DumpBytecodeVisitor(writer).visitClass(classDefinition);
+        return writer.toString();
+    }
+
+    public static void checkArgument(boolean condition, String errMsg, Object... params) {
+        if (!condition)
+            throw new IllegalArgumentException(String.format(errMsg, params));
+    }
+
+    public static void checkState(boolean expression, @Nullable Object errorMessage) {
+        if (!expression) {
+            throw new IllegalStateException(String.valueOf(errorMessage));
+        }
+    }
+
+    public static void checkState(
+        boolean expression,
+        @Nullable String errorMessageTemplate,
+        Object... errorMessageArgs) {
+        if (!expression) {
+            throw new IllegalStateException(String.format(errorMessageTemplate, errorMessageArgs));
+        }
+    }
+
+    public static <T> Class<T> wrap(Class<T> c) {
+        return c.isPrimitive() ? (Class<T>)PRIMITIVES_TO_WRAPPERS.get(c) : c;
+    }
+
+    public static String repeat(String string, int count) {
+        Objects.requireNonNull(string); // eager for GWT.
+
+        if (count <= 1) {
+            checkArgument(count >= 0, "invalid count: %s", count);
+            return (count == 0) ? "" : string;
+        }
+
+        // IF YOU MODIFY THE CODE HERE, you must update StringsRepeatBenchmark
+        final int len = string.length();
+        final long longSize = (long)len * (long)count;
+        final int size = (int)longSize;
+        if (size != longSize) {
+            throw new ArrayIndexOutOfBoundsException("Required array size too large: " + longSize);
+        }
+
+        final char[] array = new char[size];
+        string.getChars(0, len, array, 0);
+        int n;
+        for (n = len; n < size - n; n <<= 1) {
+            System.arraycopy(array, 0, array, n, n);
+        }
+        System.arraycopy(array, 0, array, n, size - n);
+        return new String(array);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeVisitor.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeVisitor.java
new file mode 100644
index 0000000..37ff572
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/BytecodeVisitor.java
@@ -0,0 +1,327 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.control.DoWhileLoop;
+import com.facebook.presto.bytecode.control.FlowControl;
+import com.facebook.presto.bytecode.control.ForLoop;
+import com.facebook.presto.bytecode.control.IfStatement;
+import com.facebook.presto.bytecode.control.SwitchStatement;
+import com.facebook.presto.bytecode.control.TryCatch;
+import com.facebook.presto.bytecode.control.WhileLoop;
+import com.facebook.presto.bytecode.debug.DebugNode;
+import com.facebook.presto.bytecode.debug.LineNumberNode;
+import com.facebook.presto.bytecode.debug.LocalVariableNode;
+import com.facebook.presto.bytecode.expression.BytecodeExpression;
+import com.facebook.presto.bytecode.instruction.Constant;
+import com.facebook.presto.bytecode.instruction.Constant.BooleanConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedBooleanConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedDoubleConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedFloatConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedIntegerConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedLongConstant;
+import com.facebook.presto.bytecode.instruction.Constant.ClassConstant;
+import com.facebook.presto.bytecode.instruction.Constant.DoubleConstant;
+import com.facebook.presto.bytecode.instruction.Constant.FloatConstant;
+import com.facebook.presto.bytecode.instruction.Constant.IntConstant;
+import com.facebook.presto.bytecode.instruction.Constant.LongConstant;
+import com.facebook.presto.bytecode.instruction.Constant.StringConstant;
+import com.facebook.presto.bytecode.instruction.FieldInstruction;
+import com.facebook.presto.bytecode.instruction.FieldInstruction.GetFieldInstruction;
+import com.facebook.presto.bytecode.instruction.FieldInstruction.PutFieldInstruction;
+import com.facebook.presto.bytecode.instruction.InstructionNode;
+import com.facebook.presto.bytecode.instruction.InvokeInstruction;
+import com.facebook.presto.bytecode.instruction.InvokeInstruction.InvokeDynamicInstruction;
+import com.facebook.presto.bytecode.instruction.JumpInstruction;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import com.facebook.presto.bytecode.instruction.VariableInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.IncrementVariableInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.LoadVariableInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.StoreVariableInstruction;
+
+public class BytecodeVisitor<T>
+{
+    public T visitClass(ClassDefinition classDefinition)
+    {
+        for (AnnotationDefinition annotationDefinition : classDefinition.getAnnotations()) {
+            visitAnnotation(classDefinition, annotationDefinition);
+        }
+        for (FieldDefinition fieldDefinition : classDefinition.getFields()) {
+            visitField(classDefinition, fieldDefinition);
+        }
+        for (MethodDefinition methodDefinition : classDefinition.getMethods()) {
+            visitMethod(classDefinition, methodDefinition);
+        }
+        return null;
+    }
+
+    public T visitAnnotation(Object parent, AnnotationDefinition annotationDefinition)
+    {
+        return null;
+    }
+
+    public T visitField(ClassDefinition classDefinition, FieldDefinition fieldDefinition)
+    {
+        for (AnnotationDefinition annotationDefinition : fieldDefinition.getAnnotations()) {
+            visitAnnotation(fieldDefinition, annotationDefinition);
+        }
+        return null;
+    }
+
+    public T visitMethod(ClassDefinition classDefinition, MethodDefinition methodDefinition)
+    {
+        for (AnnotationDefinition annotationDefinition : methodDefinition.getAnnotations()) {
+            visitAnnotation(methodDefinition, annotationDefinition);
+        }
+        methodDefinition.getBody().accept(null, this);
+        return null;
+    }
+
+    public T visitNode(BytecodeNode parent, BytecodeNode node)
+    {
+        for (BytecodeNode child : node.getChildNodes()) {
+            child.accept(node, this);
+        }
+        return null;
+    }
+
+    //
+    // Comment
+    //
+
+    public T visitComment(BytecodeNode parent, Comment node)
+    {
+        return visitNode(parent, node);
+    }
+
+    //
+    // Block
+    //
+
+    public T visitBlock(BytecodeNode parent, BytecodeBlock block)
+    {
+        return visitNode(parent, block);
+    }
+
+    //
+    // Bytecode Expression
+    //
+    public T visitBytecodeExpression(BytecodeNode parent, BytecodeExpression expression)
+    {
+        return visitNode(parent, expression);
+    }
+
+    //
+    // Flow Control
+    //
+
+    public T visitFlowControl(BytecodeNode parent, FlowControl flowControl)
+    {
+        return visitNode(parent, flowControl);
+    }
+
+    public T visitTryCatch(BytecodeNode parent, TryCatch tryCatch)
+    {
+        return visitFlowControl(parent, tryCatch);
+    }
+
+    public T visitIf(BytecodeNode parent, IfStatement ifStatement)
+    {
+        return visitFlowControl(parent, ifStatement);
+    }
+
+    public T visitFor(BytecodeNode parent, ForLoop forLoop)
+    {
+        return visitFlowControl(parent, forLoop);
+    }
+
+    public T visitWhile(BytecodeNode parent, WhileLoop whileLoop)
+    {
+        return visitFlowControl(parent, whileLoop);
+    }
+
+    public T visitDoWhile(BytecodeNode parent, DoWhileLoop doWhileLoop)
+    {
+        return visitFlowControl(parent, doWhileLoop);
+    }
+
+    public T visitSwitch(BytecodeNode parent, SwitchStatement switchStatement)
+    {
+        return visitFlowControl(parent, switchStatement);
+    }
+
+    //
+    // Instructions
+    //
+
+    public T visitInstruction(BytecodeNode parent, InstructionNode node)
+    {
+        return visitNode(parent, node);
+    }
+
+    public T visitLabel(BytecodeNode parent, LabelNode labelNode)
+    {
+        return visitInstruction(parent, labelNode);
+    }
+
+    public T visitJumpInstruction(BytecodeNode parent, JumpInstruction jumpInstruction)
+    {
+        return visitInstruction(parent, jumpInstruction);
+    }
+
+    //
+    // Constants
+    //
+
+    public T visitConstant(BytecodeNode parent, Constant constant)
+    {
+        return visitInstruction(parent, constant);
+    }
+
+    public T visitBoxedBooleanConstant(BytecodeNode parent, BoxedBooleanConstant boxedBooleanConstant)
+    {
+        return visitConstant(parent, boxedBooleanConstant);
+    }
+
+    public T visitBooleanConstant(BytecodeNode parent, BooleanConstant booleanConstant)
+    {
+        return visitConstant(parent, booleanConstant);
+    }
+
+    public T visitIntConstant(BytecodeNode parent, IntConstant intConstant)
+    {
+        return visitConstant(parent, intConstant);
+    }
+
+    public T visitBoxedIntegerConstant(BytecodeNode parent, BoxedIntegerConstant boxedIntegerConstant)
+    {
+        return visitConstant(parent, boxedIntegerConstant);
+    }
+
+    public T visitFloatConstant(BytecodeNode parent, FloatConstant floatConstant)
+    {
+        return visitConstant(parent, floatConstant);
+    }
+
+    public T visitBoxedFloatConstant(BytecodeNode parent, BoxedFloatConstant boxedFloatConstant)
+    {
+        return visitConstant(parent, boxedFloatConstant);
+    }
+
+    public T visitLongConstant(BytecodeNode parent, LongConstant longConstant)
+    {
+        return visitConstant(parent, longConstant);
+    }
+
+    public T visitBoxedLongConstant(BytecodeNode parent, BoxedLongConstant boxedLongConstant)
+    {
+        return visitConstant(parent, boxedLongConstant);
+    }
+
+    public T visitDoubleConstant(BytecodeNode parent, DoubleConstant doubleConstant)
+    {
+        return visitConstant(parent, doubleConstant);
+    }
+
+    public T visitBoxedDoubleConstant(BytecodeNode parent, BoxedDoubleConstant boxedDoubleConstant)
+    {
+        return visitConstant(parent, boxedDoubleConstant);
+    }
+
+    public T visitStringConstant(BytecodeNode parent, StringConstant stringConstant)
+    {
+        return visitConstant(parent, stringConstant);
+    }
+
+    public T visitClassConstant(BytecodeNode parent, ClassConstant classConstant)
+    {
+        return visitConstant(parent, classConstant);
+    }
+
+    //
+    // Local Variable Instructions
+    //
+
+    public T visitVariableInstruction(BytecodeNode parent, VariableInstruction variableInstruction)
+    {
+        return visitInstruction(parent, variableInstruction);
+    }
+
+    public T visitLoadVariable(BytecodeNode parent, LoadVariableInstruction loadVariableInstruction)
+    {
+        return visitVariableInstruction(parent, loadVariableInstruction);
+    }
+
+    public T visitStoreVariable(BytecodeNode parent, StoreVariableInstruction storeVariableInstruction)
+    {
+        return visitVariableInstruction(parent, storeVariableInstruction);
+    }
+
+    public T visitIncrementVariable(BytecodeNode parent, IncrementVariableInstruction incrementVariableInstruction)
+    {
+        return visitVariableInstruction(parent, incrementVariableInstruction);
+    }
+
+    //
+    // Field Instructions
+    //
+
+    public T visitFieldInstruction(BytecodeNode parent, FieldInstruction fieldInstruction)
+    {
+        return visitInstruction(parent, fieldInstruction);
+    }
+
+    public T visitGetField(BytecodeNode parent, GetFieldInstruction getFieldInstruction)
+    {
+        return visitFieldInstruction(parent, getFieldInstruction);
+    }
+
+    public T visitPutField(BytecodeNode parent, PutFieldInstruction putFieldInstruction)
+    {
+        return visitFieldInstruction(parent, putFieldInstruction);
+    }
+
+    //
+    // Invoke
+    //
+
+    public T visitInvoke(BytecodeNode parent, InvokeInstruction invokeInstruction)
+    {
+        return visitInstruction(parent, invokeInstruction);
+    }
+
+    public T visitInvokeDynamic(BytecodeNode parent, InvokeDynamicInstruction invokeDynamicInstruction)
+    {
+        return visitInvoke(parent, invokeDynamicInstruction);
+    }
+
+    //
+    // Debug
+    //
+
+    public T visitDebug(BytecodeNode parent, DebugNode debugNode)
+    {
+        return visitNode(parent, debugNode);
+    }
+
+    public T visitLineNumber(BytecodeNode parent, LineNumberNode lineNumberNode)
+    {
+        return visitDebug(parent, lineNumberNode);
+    }
+
+    public T visitLocalVariable(BytecodeNode parent, LocalVariableNode localVariableNode)
+    {
+        return visitDebug(parent, localVariableNode);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassDefinition.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassDefinition.java
new file mode 100644
index 0000000..319e304
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassDefinition.java
@@ -0,0 +1,281 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.objectweb.asm.ClassVisitor;
+
+import static com.facebook.presto.bytecode.Access.INTERFACE;
+import static com.facebook.presto.bytecode.Access.STATIC;
+import static com.facebook.presto.bytecode.Access.a;
+import static com.facebook.presto.bytecode.Access.toAccessModifier;
+import static java.util.Objects.requireNonNull;
+import static org.objectweb.asm.Opcodes.ACC_SUPER;
+import static org.objectweb.asm.Opcodes.V11;
+
+public class ClassDefinition {
+    private final EnumSet<Access> access;
+    private final ParameterizedType type;
+    private final ParameterizedType superClass;
+    private final List<ParameterizedType> interfaces = new ArrayList<>();
+    private final List<AnnotationDefinition> annotations = new ArrayList<>();
+    private final List<FieldDefinition> fields = new ArrayList<>();
+    private final List<MethodDefinition> methods = new ArrayList<>();
+    private final MethodDefinition classInitializer;
+    private String source;
+    private String debug;
+
+    public ClassDefinition(
+        EnumSet<Access> access,
+        String name,
+        ParameterizedType superClass,
+        ParameterizedType... interfaces) {
+        this(access, new ParameterizedType(name), superClass, interfaces);
+    }
+
+    public ClassDefinition(
+        EnumSet<Access> access,
+        ParameterizedType type,
+        ParameterizedType superClass,
+        ParameterizedType... interfaces) {
+        requireNonNull(access, "access is null");
+        requireNonNull(type, "type is null");
+        requireNonNull(superClass, "superClass is null");
+        requireNonNull(interfaces, "interfaces is null");
+
+        this.access = access;
+        this.type = type;
+        this.superClass = superClass;
+        this.interfaces.addAll(List.of(interfaces));
+
+        classInitializer = new MethodDefinition(this, a(STATIC), "<clinit>", ParameterizedType.type(void.class), List.of());
+    }
+
+    public Set<Access> getAccess() {
+        return Set.copyOf(access);
+    }
+
+    public String getName() {
+        return type.getClassName();
+    }
+
+    public ParameterizedType getType() {
+        return type;
+    }
+
+    public ParameterizedType getSuperClass() {
+        return superClass;
+    }
+
+    public String getSource() {
+        return source;
+    }
+
+    public List<ParameterizedType> getInterfaces() {
+        return List.copyOf(interfaces);
+    }
+
+    public List<AnnotationDefinition> getAnnotations() {
+        return List.copyOf(annotations);
+    }
+
+    public List<FieldDefinition> getFields() {
+        return List.copyOf(fields);
+    }
+
+    public List<MethodDefinition> getMethods() {
+        return List.copyOf(methods);
+    }
+
+    public boolean isInterface() {
+        return access.contains(INTERFACE);
+    }
+
+    public void visit(ClassVisitor visitor) {
+        // Generic signature if super class or any interface is generic
+        String signature = null;
+        if (superClass.isGeneric() || interfaces.stream().anyMatch(ParameterizedType::isGeneric)) {
+            signature = genericClassSignature(superClass, interfaces);
+        }
+
+        String[] interfaces = new String[this.interfaces.size()];
+        for (int i = 0; i < interfaces.length; i++) {
+            interfaces[i] = this.interfaces.get(i).getClassName();
+        }
+        int accessModifier = toAccessModifier(access);
+        visitor.visit(V11, isInterface() ? accessModifier : accessModifier | ACC_SUPER, type.getClassName(), signature, superClass.getClassName(), interfaces);
+
+        // visit source
+        if (source != null) {
+            visitor.visitSource(source, debug);
+        }
+
+        // visit annotations
+        for (AnnotationDefinition annotation : annotations) {
+            annotation.visitClassAnnotation(visitor);
+        }
+
+        // visit fields
+        for (FieldDefinition field : fields) {
+            field.visit(visitor);
+        }
+
+        // visit clinit method
+        if (!isInterface()) {
+            classInitializer.visit(visitor, true);
+        }
+
+        // visit methods
+        for (MethodDefinition method : methods) {
+            method.visit(visitor);
+        }
+
+        // done
+        visitor.visitEnd();
+    }
+
+    public AnnotationDefinition declareAnnotation(Class<?> type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public AnnotationDefinition declareAnnotation(ParameterizedType type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public FieldDefinition declareField(EnumSet<Access> access, String name, Class<?> type) {
+        FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type);
+        fields.add(fieldDefinition);
+        return fieldDefinition;
+    }
+
+    public ClassDefinition addField(EnumSet<Access> access, String name, Class<?> type) {
+        declareField(access, name, type);
+        return this;
+    }
+
+    public FieldDefinition declareField(EnumSet<Access> access, String name, ParameterizedType type) {
+        FieldDefinition fieldDefinition = new FieldDefinition(this, access, name, type);
+        fields.add(fieldDefinition);
+        return fieldDefinition;
+    }
+
+    public ClassDefinition addField(EnumSet<Access> access, String name, ParameterizedType type) {
+        declareField(access, name, type);
+        return this;
+    }
+
+    public ClassDefinition addField(FieldDefinition field) {
+        fields.add(field);
+        return this;
+    }
+
+    public MethodDefinition getClassInitializer() {
+        if (isInterface()) {
+            throw new IllegalAccessError("Interface does not have class initializer");
+        }
+        return classInitializer;
+    }
+
+    public MethodDefinition declareConstructor(
+        EnumSet<Access> access,
+        Parameter... parameters) {
+        return declareMethod(access, "<init>", ParameterizedType.type(void.class), List.of(parameters));
+    }
+
+    public MethodDefinition declareConstructor(
+        EnumSet<Access> access,
+        Collection<Parameter> parameters) {
+        return declareMethod(access, "<init>", ParameterizedType.type(void.class), List.copyOf(parameters));
+    }
+
+    public ClassDefinition declareDefaultConstructor(EnumSet<Access> access) {
+        MethodDefinition constructor = declareConstructor(access);
+        constructor
+            .getBody()
+            .append(constructor.getThis())
+            .invokeConstructor(superClass)
+            .ret();
+        return this;
+    }
+
+    public ClassDefinition addMethod(MethodDefinition method) {
+        methods.add(method);
+        return this;
+    }
+
+    public ClassDefinition visitSource(String source, String debug) {
+        this.source = source;
+        this.debug = debug;
+        return this;
+    }
+
+    public MethodDefinition declareMethod(
+        EnumSet<Access> access,
+        String name,
+        ParameterizedType returnType,
+        Parameter... parameters) {
+        return declareMethod(access, name, returnType, List.of(parameters));
+    }
+
+    public MethodDefinition declareMethod(
+        EnumSet<Access> access,
+        String name,
+        ParameterizedType returnType,
+        Collection<Parameter> parameters) {
+        MethodDefinition methodDefinition = new MethodDefinition(this, access, name, returnType, parameters);
+        for (MethodDefinition method : methods) {
+            if (name.equals(method.getName()) && method.getParameterTypes().equals(methodDefinition.getParameterTypes())) {
+                throw new IllegalArgumentException("Method with same name and signature already exists: " + name);
+            }
+        }
+        methods.add(methodDefinition);
+        return methodDefinition;
+    }
+
+    public static String genericClassSignature(
+        ParameterizedType classType,
+        ParameterizedType... interfaceTypes) {
+
+        return Stream.concat(Stream.of(classType), Stream.of(interfaceTypes))
+            .map(ParameterizedType::toString).collect(Collectors.joining(""));
+    }
+
+    public static String genericClassSignature(
+        ParameterizedType classType,
+        List<ParameterizedType> interfaceTypes) {
+
+        return Stream.concat(Stream.of(classType), interfaceTypes.stream())
+            .map(ParameterizedType::toString).collect(Collectors.joining(""));
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder sb = new StringBuilder();
+        sb.append("ClassDefinition");
+        sb.append("{access=").append(access);
+        sb.append(", type=").append(type);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassGenerator.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassGenerator.java
new file mode 100644
index 0000000..aa2389f
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassGenerator.java
@@ -0,0 +1,204 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.io.UncheckedIOException;
+import java.io.Writer;
+import java.lang.invoke.MethodHandle;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassTooLargeException;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.MethodTooLargeException;
+import org.objectweb.asm.util.CheckClassAdapter;
+import org.objectweb.asm.util.Textifier;
+import org.objectweb.asm.util.TraceClassVisitor;
+
+import static com.facebook.presto.bytecode.ClassInfoLoader.createClassInfoLoader;
+import static com.facebook.presto.bytecode.ParameterizedType.typeFromJavaClassName;
+import static java.io.Writer.nullWriter;
+import static java.nio.file.Files.createDirectories;
+import static java.util.Objects.requireNonNull;
+
+public class ClassGenerator
+{
+    private final DynamicClassLoader classLoader;
+    private final boolean fakeLineNumbers;
+    private final boolean runAsmVerifier;
+    private final boolean dumpRawBytecode;
+    private final Writer output;
+    private final Optional<Path> dumpClassPath;
+
+    public static ClassGenerator classGenerator(ClassLoader parentClassLoader)
+    {
+        if (parentClassLoader instanceof DynamicClassLoader)
+            return classGenerator((DynamicClassLoader)parentClassLoader);
+
+        return classGenerator(parentClassLoader, Map.of());
+    }
+
+    public static ClassGenerator classGenerator(ClassLoader parentClassLoader, Map<Long, MethodHandle> callSiteBindings)
+    {
+        return classGenerator(new DynamicClassLoader(parentClassLoader, callSiteBindings));
+    }
+
+    public static ClassGenerator classGenerator(DynamicClassLoader classLoader)
+    {
+        return new ClassGenerator(classLoader, false, false, false, nullWriter(), Optional.empty());
+    }
+
+    private ClassGenerator(
+            DynamicClassLoader classLoader,
+            boolean fakeLineNumbers,
+            boolean runAsmVerifier,
+            boolean dumpRawBytecode,
+            Writer output,
+            Optional<Path> dumpClassPath)
+    {
+        this.classLoader = requireNonNull(classLoader, "classLoader is null");
+        this.fakeLineNumbers = fakeLineNumbers;
+        this.runAsmVerifier = runAsmVerifier;
+        this.dumpRawBytecode = dumpRawBytecode;
+        this.output = requireNonNull(output, "output is null");
+        this.dumpClassPath = requireNonNull(dumpClassPath, "dumpClassPath is null");
+    }
+
+    public ClassGenerator fakeLineNumbers(boolean fakeLineNumbers)
+    {
+        return new ClassGenerator(classLoader, fakeLineNumbers, runAsmVerifier, dumpRawBytecode, output, dumpClassPath);
+    }
+
+    public ClassGenerator runAsmVerifier(boolean runAsmVerifier)
+    {
+        return new ClassGenerator(classLoader, fakeLineNumbers, runAsmVerifier, dumpRawBytecode, output, dumpClassPath);
+    }
+
+    public ClassGenerator dumpRawBytecode(boolean dumpRawBytecode)
+    {
+        return new ClassGenerator(classLoader, fakeLineNumbers, runAsmVerifier, dumpRawBytecode, output, dumpClassPath);
+    }
+
+    public ClassGenerator outputTo(Writer output)
+    {
+        return new ClassGenerator(classLoader, fakeLineNumbers, runAsmVerifier, dumpRawBytecode, output, dumpClassPath);
+    }
+
+    public ClassGenerator dumpClassFilesTo(Path dumpClassPath)
+    {
+        return dumpClassFilesTo(Optional.of(dumpClassPath));
+    }
+
+    public ClassGenerator dumpClassFilesTo(Optional<Path> dumpClassPath)
+    {
+        return new ClassGenerator(classLoader, fakeLineNumbers, runAsmVerifier, dumpRawBytecode, output, dumpClassPath);
+    }
+
+    public <T> Class<? extends T> defineClass(ClassDefinition classDefinition, Class<T> superType)
+    {
+        Map<String, Class<?>> classes = defineClasses(List.of(classDefinition));
+
+        return classes.values().stream().findFirst().get().asSubclass(superType);
+    }
+
+    public Map<String, Class<?>> defineClasses(List<ClassDefinition> classDefinitions)
+    {
+        ClassInfoLoader classInfoLoader = createClassInfoLoader(classDefinitions, classLoader);
+        Map<String, byte[]> bytecodes = new LinkedHashMap<>();
+
+        for (ClassDefinition classDefinition : classDefinitions) {
+            // We call the simpler class writer first to get any errors out using simpler setting.
+            // This helps when we have large queries that can potentially cause COMPUTE_FRAMES
+            // (used by SmartClassWriter for doing more thorough analysis)
+            ClassWriter simpleClassWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
+            classDefinition.visit(simpleClassWriter);
+            try {
+                simpleClassWriter.toByteArray();
+            }
+            catch (ClassTooLargeException | MethodTooLargeException largeCodeException) {
+                throw new ByteCodeTooLargeException(largeCodeException);
+            }
+            catch (RuntimeException e) {
+                throw new CompilationException("Error compiling class: " + classDefinition.getName(), e);
+            }
+
+            ClassWriter writer = new SmartClassWriter(classInfoLoader);
+
+            try {
+                classDefinition.visit(fakeLineNumbers ? new AddFakeLineNumberClassVisitor(writer) : writer);
+            }
+            catch (IndexOutOfBoundsException | NegativeArraySizeException e) {
+                StringWriter out = new StringWriter();
+                classDefinition.visit(new TraceClassVisitor(null, new Textifier(), new PrintWriter(out)));
+                throw new IllegalArgumentException("Error processing class definition:\n" + out, e);
+            }
+
+            byte[] bytecode;
+            try {
+                bytecode = writer.toByteArray();
+            }
+            catch (ClassTooLargeException | MethodTooLargeException largeCodeException) {
+                throw new ByteCodeTooLargeException(largeCodeException);
+            }
+            catch (RuntimeException e) {
+                throw new CompilationException("Error compiling class: " + classDefinition.getName(), e);
+            }
+
+            bytecodes.put(classDefinition.getType().getJavaClassName(), bytecode);
+
+            if (runAsmVerifier) {
+                ClassReader reader = new ClassReader(bytecode);
+                CheckClassAdapter.verify(reader, classLoader, true, new PrintWriter(output));
+            }
+        }
+
+        dumpClassPath.ifPresent(path -> bytecodes.forEach((className, bytecode) -> {
+            String name = typeFromJavaClassName(className).getClassName() + ".class";
+            Path file = path.resolve(name).toAbsolutePath();
+            try {
+                createDirectories(file.getParent());
+                Files.write(file, bytecode);
+            }
+            catch (IOException e) {
+                throw new UncheckedIOException("Failed to write generated class file: " + file, e);
+            }
+        }));
+
+        if (dumpRawBytecode) {
+            for (byte[] bytecode : bytecodes.values()) {
+                ClassReader classReader = new ClassReader(bytecode);
+                classReader.accept(new TraceClassVisitor(new PrintWriter(output)), ClassReader.EXPAND_FRAMES);
+            }
+        }
+
+        Map<String, Class<?>> classes = classLoader.defineClasses(bytecodes);
+
+        try {
+            for (Class<?> clazz : classes.values()) {
+                Class.forName(clazz.getName(), true, clazz.getClassLoader());
+            }
+        } catch (ClassNotFoundException | VerifyError e) {
+            throw new RuntimeException(e);
+        }
+
+        return classes;
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfo.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfo.java
new file mode 100644
index 0000000..b9b6f64
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfo.java
@@ -0,0 +1,169 @@
+/***
+ * ASM tests
+ * Copyright (c) 2002-2005 France Telecom
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holders nor the names of its
+ *    contributors may be used to endorse or promote products derived from
+ *    this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.facebook.presto.bytecode;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.tree.MethodNode;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.ParameterizedType.typeFromPathName;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * @author Eugene Kuleshov
+ */
+public class ClassInfo {
+    private final ClassInfoLoader loader;
+    private final ParameterizedType type;
+    private final int access;
+    private final ParameterizedType superClass;
+    private final List<ParameterizedType> interfaces;
+    private final List<MethodNode> methods;
+
+    public ClassInfo(ClassInfoLoader loader, ClassNode classNode) {
+        this(loader,
+            typeFromPathName(classNode.name),
+            classNode.access,
+            classNode.superName == null ? null : typeFromPathName(classNode.superName),
+            classNode.interfaces.stream().map(ParameterizedType::typeFromPathName).collect(Collectors.toList()),
+            classNode.methods);
+    }
+
+    public ClassInfo(ClassInfoLoader loader, Class<?> aClass) {
+        this(loader,
+            type(aClass),
+            aClass.getModifiers(),
+            aClass.getSuperclass() == null ? null : type(aClass.getSuperclass()),
+            stream(aClass.getInterfaces()).map(ParameterizedType::type).collect(Collectors.toList()),
+            null);
+    }
+
+    public ClassInfo(ClassInfoLoader loader, ParameterizedType type, int access, ParameterizedType superClass,
+        Collection<ParameterizedType> interfaces, Collection<MethodNode> methods) {
+        requireNonNull(loader, "loader is null");
+        requireNonNull(type, "type is null");
+        requireNonNull(interfaces, "interfaces is null");
+
+        this.loader = loader;
+        this.type = type;
+        this.access = access;
+        this.superClass = superClass;
+        this.interfaces = List.copyOf(interfaces);
+        if (methods != null) {
+            this.methods = List.copyOf(methods);
+        }
+        else {
+            this.methods = null;
+        }
+    }
+
+    public ParameterizedType getType() {
+        return type;
+    }
+
+    public int getModifiers() {
+        return access;
+    }
+
+    public ClassInfo getSuperclass() {
+        if (superClass == null) {
+            return null;
+        }
+        return loader.loadClassInfo(superClass);
+    }
+
+    public List<ClassInfo> getInterfaces() {
+        if (interfaces == null) {
+            return List.of();
+        }
+        return interfaces.stream().map(loader::loadClassInfo).collect(Collectors.toList());
+    }
+
+    public List<MethodNode> getMethods() {
+        checkState(methods != null, "Methods were not loaded for type %s", type);
+        return methods;
+    }
+
+    boolean isInterface() {
+        return (getModifiers() & Opcodes.ACC_INTERFACE) > 0;
+    }
+
+    private boolean implementsInterface(ClassInfo that) {
+        for (ClassInfo classInfo = this; classInfo != null; classInfo = classInfo.getSuperclass()) {
+            for (ClassInfo anInterface : classInfo.getInterfaces()) {
+                if (anInterface.type.equals(that.type) || anInterface.implementsInterface(that)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private boolean isSubclassOf(ClassInfo that) {
+        for (ClassInfo classInfo = this; classInfo != null; classInfo = classInfo.getSuperclass()) {
+            if (classInfo.getSuperclass() != null &&
+                classInfo.getSuperclass().type.equals(that.type)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public boolean isAssignableFrom(ClassInfo that) {
+        if (this == that) {
+            return true;
+        }
+
+        if (that.isSubclassOf(this)) {
+            return true;
+        }
+
+        if (that.implementsInterface(this)) {
+            return true;
+        }
+
+        if (that.isInterface() && getType().equals(type(Object.class))) {
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return type.toString();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfoLoader.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfoLoader.java
new file mode 100644
index 0000000..75f215c
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ClassInfoLoader.java
@@ -0,0 +1,147 @@
+/***
+ * ASM tests
+ * Copyright (c) 2002-2005 France Telecom
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the copyright holders nor the names of its
+ *    contributors may be used to endorse or promote products derived from
+ *    this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+ * THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.facebook.presto.bytecode;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.tree.ClassNode;
+import org.objectweb.asm.util.CheckClassAdapter;
+
+import static com.facebook.presto.bytecode.ParameterizedType.typeFromPathName;
+
+public class ClassInfoLoader {
+    public static ClassInfoLoader createClassInfoLoader(Collection<ClassDefinition> classDefinitions,
+        ClassLoader classLoader) {
+        Map<ParameterizedType, ClassNode> classNodes = new HashMap<>();
+        for (ClassDefinition classDefinition : classDefinitions) {
+            ClassNode classNode = new ClassNode();
+            classDefinition.visit(classNode);
+            classNodes.put(classDefinition.getType(), classNode);
+        }
+        return new ClassInfoLoader(Collections.unmodifiableMap(classNodes), Map.of(), classLoader, true);
+    }
+
+    private final Map<ParameterizedType, ClassNode> classNodes;
+    private final Map<ParameterizedType, byte[]> bytecodes;
+    private final ClassLoader classLoader;
+    private final Map<ParameterizedType, ClassInfo> classInfoCache = new HashMap<>();
+    private final boolean loadMethodNodes;
+
+    public ClassInfoLoader(Map<ParameterizedType, ClassNode> classNodes, Map<ParameterizedType, byte[]> bytecodes,
+        ClassLoader classLoader, boolean loadMethodNodes) {
+        this.classNodes = Map.copyOf(classNodes);
+        this.bytecodes = Map.copyOf(bytecodes);
+        this.classLoader = classLoader;
+        this.loadMethodNodes = loadMethodNodes;
+    }
+
+    public ClassInfo loadClassInfo(ParameterizedType type) {
+        ClassInfo classInfo = classInfoCache.get(type);
+        if (classInfo == null) {
+            classInfo = readClassInfoQuick(type);
+            classInfoCache.put(type, classInfo);
+        }
+        return classInfo;
+    }
+
+    private ClassInfo readClassInfoQuick(ParameterizedType type) {
+        // check for user supplied class node
+        ClassNode classNode = classNodes.get(type);
+        if (classNode != null) {
+            return new ClassInfo(this, classNode);
+        }
+
+        // check for user supplied byte code
+        ClassReader classReader;
+        byte[] bytecode = bytecodes.get(type);
+        if (bytecode != null) {
+            classReader = new ClassReader(bytecode);
+        }
+        else {
+            // load class file from class loader
+            String classFileName = type.getClassName() + ".class";
+            try (InputStream is = classLoader.getResourceAsStream(classFileName)) {
+                classReader = new ClassReader(is);
+            }
+            catch (IOException e) {
+                // check if class is already loaded
+                try {
+                    Class<?> aClass = classLoader.loadClass(type.getJavaClassName());
+                    return new ClassInfo(this, aClass);
+                }
+                catch (ClassNotFoundException e1) {
+                    throw new RuntimeException("Class not found " + type, e);
+                }
+            }
+        }
+
+        if (loadMethodNodes) {
+            // slower version that loads all operations
+            classNode = new ClassNode();
+            classReader.accept(new CheckClassAdapter(classNode, false), ClassReader.SKIP_DEBUG);
+
+            return new ClassInfo(this, classNode);
+        }
+        else {
+            // optimized version
+            int header = classReader.header;
+            int access = classReader.readUnsignedShort(header);
+
+            char[] buf = new char[2048];
+
+            // read super class name
+            int superClassIndex = classReader.getItem(classReader.readUnsignedShort(header + 4));
+            ParameterizedType superClass;
+            if (superClassIndex == 0) {
+                superClass = null;
+            }
+            else {
+                superClass = typeFromPathName(classReader.readUTF8(superClassIndex, buf));
+            }
+
+            // read each interface name
+            int interfaceCount = classReader.readUnsignedShort(header + 6);
+            List<ParameterizedType> interfaces = new ArrayList<>();
+            header += 8;
+            for (int i = 0; i < interfaceCount; ++i) {
+                interfaces.add(typeFromPathName(classReader.readClass(header, buf)));
+                header += 2;
+            }
+            return new ClassInfo(this, type, access, superClass, interfaces, null);
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Comment.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Comment.java
new file mode 100644
index 0000000..761e9a6
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Comment.java
@@ -0,0 +1,50 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.instruction.InstructionNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+public class Comment
+    implements InstructionNode {
+    protected final String comment;
+
+    public Comment(String comment) {
+        this.comment = comment;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + '{' + comment + '}';
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitComment(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/CompilationException.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/CompilationException.java
new file mode 100644
index 0000000..f1483cd
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/CompilationException.java
@@ -0,0 +1,23 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+public class CompilationException
+        extends RuntimeException
+{
+    public CompilationException(String message, RuntimeException cause)
+    {
+        super(message, cause);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DumpBytecodeVisitor.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DumpBytecodeVisitor.java
new file mode 100644
index 0000000..bc79f0c
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DumpBytecodeVisitor.java
@@ -0,0 +1,601 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.control.CaseStatement;
+import com.facebook.presto.bytecode.control.DoWhileLoop;
+import com.facebook.presto.bytecode.control.ForLoop;
+import com.facebook.presto.bytecode.control.IfStatement;
+import com.facebook.presto.bytecode.control.SwitchStatement;
+import com.facebook.presto.bytecode.control.TryCatch;
+import com.facebook.presto.bytecode.control.WhileLoop;
+import com.facebook.presto.bytecode.debug.LineNumberNode;
+import com.facebook.presto.bytecode.expression.BytecodeExpression;
+import com.facebook.presto.bytecode.instruction.Constant.BooleanConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedBooleanConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedDoubleConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedFloatConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedIntegerConstant;
+import com.facebook.presto.bytecode.instruction.Constant.BoxedLongConstant;
+import com.facebook.presto.bytecode.instruction.Constant.ClassConstant;
+import com.facebook.presto.bytecode.instruction.Constant.DoubleConstant;
+import com.facebook.presto.bytecode.instruction.Constant.FloatConstant;
+import com.facebook.presto.bytecode.instruction.Constant.IntConstant;
+import com.facebook.presto.bytecode.instruction.Constant.LongConstant;
+import com.facebook.presto.bytecode.instruction.Constant.StringConstant;
+import com.facebook.presto.bytecode.instruction.InvokeInstruction;
+import com.facebook.presto.bytecode.instruction.InvokeInstruction.InvokeDynamicInstruction;
+import com.facebook.presto.bytecode.instruction.JumpInstruction;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.IncrementVariableInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.LoadVariableInstruction;
+import com.facebook.presto.bytecode.instruction.VariableInstruction.StoreVariableInstruction;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.facebook.presto.bytecode.Access.INTERFACE;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.lang.String.format;
+
+public class DumpBytecodeVisitor
+        extends BytecodeVisitor<Void>
+{
+    private final PrintWriter out;
+    private int indentLevel;
+
+    public DumpBytecodeVisitor(Writer out)
+    {
+        this.out = new PrintWriter(out);
+        indentLevel = 0;
+    }
+
+    @Override
+    public Void visitClass(ClassDefinition classDefinition)
+    {
+        // print annotations first
+        for (AnnotationDefinition annotationDefinition : classDefinition.getAnnotations()) {
+            visitAnnotation(classDefinition, annotationDefinition);
+        }
+
+        // print class declaration
+        Line classDeclaration = line().addAll(classDefinition.getAccess());
+        if (!classDefinition.getAccess().contains(INTERFACE)) {
+            classDeclaration.add("class");
+        }
+        classDeclaration.add(classDefinition.getType().getJavaClassName());
+        if (!classDefinition.getSuperClass().equals(type(Object.class))) {
+            classDeclaration.add("extends").add(classDefinition.getSuperClass().getJavaClassName());
+        }
+        if (!classDefinition.getInterfaces().isEmpty()) {
+            classDeclaration.add("implements");
+            for (ParameterizedType interfaceType : classDefinition.getInterfaces()) {
+                classDeclaration.add(interfaceType.getJavaClassName());
+            }
+        }
+        classDeclaration.print();
+
+        // print class body
+        printLine("{");
+        indentLevel++;
+
+        // print fields
+        for (FieldDefinition fieldDefinition : classDefinition.getFields()) {
+            visitField(classDefinition, fieldDefinition);
+        }
+
+        // print methods
+        for (MethodDefinition methodDefinition : classDefinition.getMethods()) {
+            visitMethod(classDefinition, methodDefinition);
+        }
+
+        // print class initializer
+        visitMethod(classDefinition, classDefinition.getClassInitializer());
+
+        indentLevel--;
+        printLine("}");
+        printLine();
+        return null;
+    }
+
+    @Override
+    public Void visitAnnotation(Object parent, AnnotationDefinition annotationDefinition)
+    {
+        printLine("@%s", annotationDefinition.getType().getJavaClassName(), annotationDefinition.getValues());
+        return null;
+    }
+
+    @Override
+    public Void visitField(ClassDefinition classDefinition, FieldDefinition fieldDefinition)
+    {
+        // print annotations first
+        for (AnnotationDefinition annotationDefinition : fieldDefinition.getAnnotations()) {
+            visitAnnotation(fieldDefinition, annotationDefinition);
+        }
+
+        // print field declaration
+        line().addAll(fieldDefinition.getAccess()).add(fieldDefinition.getType().getJavaClassName()).add(fieldDefinition.getName()).add(";").print();
+
+        printLine();
+        return null;
+    }
+
+    @Override
+    public Void visitMethod(ClassDefinition classDefinition, MethodDefinition methodDefinition)
+    {
+        if (methodDefinition.getComment() != null) {
+            printLine("// %s", methodDefinition.getComment());
+        }
+        // print annotations first
+        for (AnnotationDefinition annotationDefinition : methodDefinition.getAnnotations()) {
+            visitAnnotation(methodDefinition, annotationDefinition);
+        }
+
+        // print method declaration
+        printLine(methodDefinition.toSourceString());
+
+        // print body
+        methodDefinition.getBody().accept(null, this);
+
+        printLine();
+        return null;
+    }
+
+    @Override
+    public Void visitComment(BytecodeNode parent, Comment node)
+    {
+        printLine();
+        printLine("// %s", node.getComment());
+        return null;
+    }
+
+    @Override
+    public Void visitBlock(BytecodeNode parent, BytecodeBlock block)
+    {
+        // only indent if we have a block description or more than one child node
+        boolean indented;
+        if (block.getDescription() != null) {
+            line().add(block.getDescription()).add("{").print();
+            indentLevel++;
+            indented = true;
+        }
+        else if (block.getChildNodes().size() > 1) {
+            printLine("{");
+            indentLevel++;
+            indented = true;
+        }
+        else {
+            indented = false;
+        }
+
+        visitBlockContents(block);
+        if (indented) {
+            indentLevel--;
+            printLine("}");
+        }
+
+        return null;
+    }
+
+    private void visitBlockContents(BytecodeBlock block)
+    {
+        for (BytecodeNode node : block.getChildNodes()) {
+            if (node instanceof BytecodeBlock) {
+                BytecodeBlock childBlock = (BytecodeBlock) node;
+                if (childBlock.getDescription() != null) {
+                    visitBlock(block, childBlock);
+                }
+                else {
+                    visitBlockContents(childBlock);
+                }
+            }
+            else {
+                node.accept(node, this);
+            }
+        }
+    }
+
+    @Override
+    public Void visitBytecodeExpression(BytecodeNode parent, BytecodeExpression expression)
+    {
+        printLine(expression.toString());
+        return null;
+    }
+
+    @Override
+    public Void visitNode(BytecodeNode parent, BytecodeNode node)
+    {
+        printLine(node.toString());
+        super.visitNode(parent, node);
+        return null;
+    }
+
+    @Override
+    public Void visitLabel(BytecodeNode parent, LabelNode labelNode)
+    {
+        printLine("%s:", labelNode.getName());
+        return null;
+    }
+
+    @Override
+    public Void visitJumpInstruction(BytecodeNode parent, JumpInstruction jumpInstruction)
+    {
+        printLine("%s %s", jumpInstruction.getOpCode(), jumpInstruction.getLabel().getName());
+        return null;
+    }
+
+    //
+    // Variable
+    //
+
+    @Override
+    public Void visitLoadVariable(BytecodeNode parent, LoadVariableInstruction loadVariableInstruction)
+    {
+        Variable variable = loadVariableInstruction.getVariable();
+        printLine("load %s", variable.getName());
+        return null;
+    }
+
+    @Override
+    public Void visitStoreVariable(BytecodeNode parent, StoreVariableInstruction storeVariableInstruction)
+    {
+        Variable variable = storeVariableInstruction.getVariable();
+        printLine("store %s)", variable.getName());
+        return null;
+    }
+
+    @Override
+    public Void visitIncrementVariable(BytecodeNode parent, IncrementVariableInstruction incrementVariableInstruction)
+    {
+        Variable variable = incrementVariableInstruction.getVariable();
+        byte increment = incrementVariableInstruction.getIncrement();
+        printLine("increment %s %s", variable.getName(), increment);
+        return null;
+    }
+
+    //
+    // Invoke
+    //
+
+    @Override
+    public Void visitInvoke(BytecodeNode parent, InvokeInstruction invokeInstruction)
+    {
+        printLine("invoke %s.%s%s",
+                invokeInstruction.getTarget().getJavaClassName(),
+                invokeInstruction.getName(),
+                invokeInstruction.getMethodDescription());
+        return null;
+    }
+
+    @Override
+    public Void visitInvokeDynamic(BytecodeNode parent, InvokeDynamicInstruction invokeDynamicInstruction)
+    {
+        printLine("invokeDynamic %s%s %s",
+                invokeDynamicInstruction.getName(),
+                invokeDynamicInstruction.getMethodDescription(),
+                invokeDynamicInstruction.getBootstrapArguments());
+        return null;
+    }
+
+    //
+    // Control Flow
+    //
+
+    @Override
+    public Void visitTryCatch(BytecodeNode parent, TryCatch tryCatch)
+    {
+        if (tryCatch.getComment() != null) {
+            printLine();
+            printLine("// %s", tryCatch.getComment());
+        }
+
+        printLine("try {");
+        indentLevel++;
+        tryCatch.getTryNode().accept(tryCatch, this);
+        indentLevel--;
+        printLine("}");
+
+        printLine("catch (%s) {", tryCatch.getExceptionName());
+        indentLevel++;
+        tryCatch.getCatchNode().accept(tryCatch, this);
+        indentLevel--;
+        printLine("}");
+
+        return null;
+    }
+
+    @Override
+    public Void visitIf(BytecodeNode parent, IfStatement ifStatement)
+    {
+        if (ifStatement.getComment() != null) {
+            printLine();
+            printLine("// %s", ifStatement.getComment());
+        }
+        printLine("if {");
+        indentLevel++;
+        visitNestedNode("condition", ifStatement.condition(), ifStatement);
+        if (!ifStatement.ifTrue().isEmpty()) {
+            visitNestedNode("ifTrue", ifStatement.ifTrue(), ifStatement);
+        }
+        if (!ifStatement.ifFalse().isEmpty()) {
+            visitNestedNode("ifFalse", ifStatement.ifFalse(), ifStatement);
+        }
+        indentLevel--;
+        printLine("}");
+        return null;
+    }
+
+    @Override
+    public Void visitFor(BytecodeNode parent, ForLoop forLoop)
+    {
+        if (forLoop.getComment() != null) {
+            printLine();
+            printLine("// %s", forLoop.getComment());
+        }
+        printLine("for {");
+        indentLevel++;
+        visitNestedNode("initialize", forLoop.initialize(), forLoop);
+        visitNestedNode("condition", forLoop.condition(), forLoop);
+        visitNestedNode("update", forLoop.update(), forLoop);
+        visitNestedNode("body", forLoop.body(), forLoop);
+        indentLevel--;
+        printLine("}");
+        return null;
+    }
+
+    @Override
+    public Void visitWhile(BytecodeNode parent, WhileLoop whileLoop)
+    {
+        if (whileLoop.getComment() != null) {
+            printLine();
+            printLine("// %s", whileLoop.getComment());
+        }
+        printLine("while {");
+        indentLevel++;
+        visitNestedNode("condition", whileLoop.condition(), whileLoop);
+        visitNestedNode("body", whileLoop.body(), whileLoop);
+        indentLevel--;
+        printLine("}");
+        return null;
+    }
+
+    @Override
+    public Void visitDoWhile(BytecodeNode parent, DoWhileLoop doWhileLoop)
+    {
+        if (doWhileLoop.getComment() != null) {
+            printLine();
+            printLine("// %s", doWhileLoop.getComment());
+        }
+        printLine("while {");
+        indentLevel++;
+        visitNestedNode("body", doWhileLoop.body(), doWhileLoop);
+        visitNestedNode("condition", doWhileLoop.condition(), doWhileLoop);
+        indentLevel--;
+        printLine("}");
+        return null;
+    }
+
+    @Override
+    public Void visitSwitch(BytecodeNode parent, SwitchStatement switchStatement)
+    {
+        if (switchStatement.getComment() != null) {
+            printLine();
+            printLine("// %s", switchStatement.getComment());
+        }
+        printLine("switch {");
+        indentLevel++;
+        visitNestedNode("expression", switchStatement.expression(), switchStatement);
+        for (CaseStatement caseStatement : switchStatement.cases()) {
+            visitNestedNode(format("case %s:", caseStatement.getKey()), caseStatement.getBody(), switchStatement);
+        }
+        if (switchStatement.getDefaultBody() != null) {
+            visitNestedNode("default:", switchStatement.getDefaultBody(), switchStatement);
+        }
+        indentLevel--;
+        printLine("}");
+        return null;
+    }
+
+    //
+    // Instructions
+    //
+
+    //
+    // Constants
+    //
+
+    @Override
+    public Void visitBoxedBooleanConstant(BytecodeNode parent, BoxedBooleanConstant boxedBooleanConstant)
+    {
+        printLine("load constant %s", boxedBooleanConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitBooleanConstant(BytecodeNode parent, BooleanConstant booleanConstant)
+    {
+        printLine("load constant %s", booleanConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitIntConstant(BytecodeNode parent, IntConstant intConstant)
+    {
+        printLine("load constant %s", intConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitBoxedIntegerConstant(BytecodeNode parent, BoxedIntegerConstant boxedIntegerConstant)
+    {
+        printLine("load constant new Integer(%s)", boxedIntegerConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitFloatConstant(BytecodeNode parent, FloatConstant floatConstant)
+    {
+        printLine("load constant %sf", floatConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitBoxedFloatConstant(BytecodeNode parent, BoxedFloatConstant boxedFloatConstant)
+    {
+        printLine("load constant new Float(%sf)", boxedFloatConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitLongConstant(BytecodeNode parent, LongConstant longConstant)
+    {
+        printLine("load constant %sL", longConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitBoxedLongConstant(BytecodeNode parent, BoxedLongConstant boxedLongConstant)
+    {
+        printLine("load constant new Long(%sL)", boxedLongConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitDoubleConstant(BytecodeNode parent, DoubleConstant doubleConstant)
+    {
+        printLine("load constant %s", doubleConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitBoxedDoubleConstant(BytecodeNode parent, BoxedDoubleConstant boxedDoubleConstant)
+    {
+        printLine("load constant new Double(%s)", boxedDoubleConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitStringConstant(BytecodeNode parent, StringConstant stringConstant)
+    {
+        printLine("load constant \"%s\"", stringConstant.getValue());
+        return null;
+    }
+
+    @Override
+    public Void visitClassConstant(BytecodeNode parent, ClassConstant classConstant)
+    {
+        printLine("load constant %s.class", classConstant.getValue().getJavaClassName());
+        return null;
+    }
+
+    //
+    // Line Number
+    //
+
+    @Override
+    public Void visitLineNumber(BytecodeNode parent, LineNumberNode lineNumberNode)
+    {
+        lineNumber = lineNumberNode.getLineNumber();
+        printLine("LINE %s", lineNumber);
+        return null;
+    }
+
+    //
+    // Print
+    //
+
+    private int lineNumber = -1;
+
+    public void printLine()
+    {
+        out.println(indent(indentLevel));
+    }
+
+    public void printLine(String line)
+    {
+        out.println(format("%s%s", indent(indentLevel), line));
+    }
+
+    public void printLine(String format, Object... args)
+    {
+        String line = format(format, args);
+        out.println(format("%s%s", indent(indentLevel), line));
+    }
+
+    public void printWords(String... words)
+    {
+        String line = String.join(" ", words);
+        out.println(format("%s%s", indent(indentLevel), line));
+    }
+
+    private String indent(int level)
+    {
+        return BytecodeUtils.repeat("    ", level);
+    }
+
+    private void visitNestedNode(String description, BytecodeNode node, BytecodeNode parent)
+    {
+        printLine(description + " {");
+        indentLevel++;
+        node.accept(parent, this);
+        indentLevel--;
+        printLine("}");
+    }
+
+    private Line line()
+    {
+        return new Line();
+    }
+
+    private class Line
+    {
+        private final String separator;
+        private final List<Object> parts = new ArrayList<>();
+
+        private Line()
+        {
+            separator = " ";
+        }
+
+        public Line add(Object element)
+        {
+            parts.add(element);
+            return this;
+        }
+
+        public Line addAll(Collection<?> c)
+        {
+            parts.addAll(c);
+            return this;
+        }
+
+        public void print()
+        {
+            printLine(parts.stream().map(this::toCharSequence).collect(Collectors.joining(separator)));
+        }
+
+        private CharSequence toCharSequence(Object p) {
+            return p instanceof CharSequence ? (CharSequence)p : p.toString();
+        }
+
+        @Override
+        public String toString()
+        {
+            return parts.stream().map(this::toCharSequence).collect(Collectors.joining(separator));
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DynamicClassLoader.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DynamicClassLoader.java
new file mode 100644
index 0000000..c33787c
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/DynamicClassLoader.java
@@ -0,0 +1,144 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.lang.invoke.MethodHandle;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.stream.Collectors;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+
+public class DynamicClassLoader
+        extends ClassLoader
+{
+    private final ConcurrentMap<String, byte[]> pendingClasses = new ConcurrentHashMap<>();
+    private final Map<Long, MethodHandle> callSiteBindings;
+    private final Optional<ClassLoader> overrideClassLoader;
+
+    public DynamicClassLoader(ClassLoader parentClassLoader)
+    {
+        this(parentClassLoader, Map.of());
+    }
+
+    // TODO: this is a hack that should be removed
+    @Deprecated
+    public DynamicClassLoader(ClassLoader overrideClassLoader, ClassLoader parentClassLoader)
+    {
+        super(parentClassLoader);
+        this.callSiteBindings = Map.of();
+        this.overrideClassLoader = Optional.of(overrideClassLoader);
+    }
+
+    public DynamicClassLoader(ClassLoader parentClassLoader, Map<Long, MethodHandle> callSiteBindings)
+    {
+        super(parentClassLoader);
+        this.callSiteBindings = Map.copyOf(callSiteBindings);
+        this.overrideClassLoader = Optional.empty();
+    }
+
+    public Class<?> defineClass(String className, byte[] bytecode)
+    {
+        return defineClass(className, bytecode, 0, bytecode.length);
+    }
+
+    public Map<String, Class<?>> defineClasses(Map<String, byte[]> newClasses)
+    {
+        final Set<String> conflicts = newClasses.keySet().stream().filter(pendingClasses::containsKey).collect(Collectors.toSet());
+
+        checkArgument(conflicts.isEmpty(), "The classes %s have already been defined", conflicts);
+
+        pendingClasses.putAll(newClasses);
+        try {
+            Map<String, Class<?>> classes = new HashMap<>();
+            for (String className : newClasses.keySet()) {
+                try {
+                    Class<?> clazz = loadClass(className);
+                    classes.put(className, clazz);
+                }
+                catch (ClassNotFoundException e) {
+                    // this should never happen
+                    throw new RuntimeException(e);
+                }
+            }
+            return classes;
+        }
+        finally {
+            pendingClasses.keySet().removeAll(newClasses.keySet());
+        }
+    }
+
+    public Map<Long, MethodHandle> getCallSiteBindings()
+    {
+        return callSiteBindings;
+    }
+
+    @Override
+    protected Class<?> findClass(String name)
+            throws ClassNotFoundException
+    {
+        byte[] bytecode = pendingClasses.get(name);
+        if (bytecode == null) {
+            throw new ClassNotFoundException(name);
+        }
+
+        return defineClass(name, bytecode);
+    }
+
+    @Override
+    protected Class<?> loadClass(String name, boolean resolve)
+            throws ClassNotFoundException
+    {
+        // grab the magic lock
+        synchronized (getClassLoadingLock(name)) {
+            // Check if class is in the loaded classes cache
+            Class<?> cachedClass = findLoadedClass(name);
+            if (cachedClass != null) {
+                return resolveClass(cachedClass, resolve);
+            }
+
+            try {
+                Class<?> clazz = findClass(name);
+                return resolveClass(clazz, resolve);
+            }
+            catch (ClassNotFoundException ignored) {
+                // not a local class
+            }
+
+            if (overrideClassLoader.isPresent()) {
+                try {
+                    return resolveClass(overrideClassLoader.get().loadClass(name), resolve);
+                }
+                catch (ClassNotFoundException e) {
+                    // not in override loader
+                }
+            }
+
+            Class<?> clazz = getParent().loadClass(name);
+            return resolveClass(clazz, resolve);
+        }
+    }
+
+    private Class<?> resolveClass(Class<?> clazz, boolean resolve)
+    {
+        if (resolve) {
+            resolveClass(clazz);
+        }
+        return clazz;
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/FieldDefinition.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/FieldDefinition.java
new file mode 100644
index 0000000..b8dd132
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/FieldDefinition.java
@@ -0,0 +1,106 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+
+import static com.facebook.presto.bytecode.Access.toAccessModifier;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+
+public class FieldDefinition {
+    private final ClassDefinition declaringClass;
+    private final Set<Access> access;
+    private final String name;
+    private final ParameterizedType type;
+    private final List<AnnotationDefinition> annotations = new ArrayList<>();
+
+    public FieldDefinition(ClassDefinition declaringClass, EnumSet<Access> access, String name,
+        ParameterizedType type) {
+        this.declaringClass = declaringClass;
+        this.access = Collections.unmodifiableSet(access);
+        this.name = name;
+        this.type = type;
+    }
+
+    public FieldDefinition(ClassDefinition declaringClass, EnumSet<Access> access, String name, Class<?> type) {
+        this(declaringClass, access, name, type(type));
+    }
+
+    public ClassDefinition getDeclaringClass() {
+        return declaringClass;
+    }
+
+    public Set<Access> getAccess() {
+        return access;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public ParameterizedType getType() {
+        return type;
+    }
+
+    public List<AnnotationDefinition> getAnnotations() {
+        return List.copyOf(annotations);
+    }
+
+    public AnnotationDefinition declareAnnotation(Class<?> type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public AnnotationDefinition declareAnnotation(ParameterizedType type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public void visit(ClassVisitor visitor) {
+        FieldVisitor fieldVisitor = visitor.visitField(toAccessModifier(access),
+            name,
+            type.getType(),
+            type.isPrimitive() ? null : type.getGenericSignature(),
+            null);
+
+        if (fieldVisitor == null) {
+            return;
+        }
+
+        for (AnnotationDefinition annotation : annotations) {
+            annotation.visitFieldAnnotation(fieldVisitor);
+        }
+
+        fieldVisitor.visitEnd();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("FieldDefinition");
+        sb.append("{access=").append(access);
+        sb.append(", name='").append(name).append('\'');
+        sb.append(", type=").append(type);
+        sb.append('}');
+        return sb.toString();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodDefinition.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodDefinition.java
new file mode 100644
index 0000000..1405765
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodDefinition.java
@@ -0,0 +1,284 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.tree.InsnNode;
+
+import static com.facebook.presto.bytecode.Access.STATIC;
+import static com.facebook.presto.bytecode.Access.toAccessModifier;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static org.objectweb.asm.Opcodes.RETURN;
+
+@SuppressWarnings("UnusedDeclaration")
+public class MethodDefinition {
+    private final Scope scope;
+    private final ClassDefinition declaringClass;
+    private final EnumSet<Access> access;
+    private final String name;
+    private final List<AnnotationDefinition> annotations = new ArrayList<>();
+    private final ParameterizedType returnType;
+    private final List<Parameter> parameters;
+    private final List<ParameterizedType> parameterTypes;
+    private final List<List<AnnotationDefinition>> parameterAnnotations;
+    private final List<ParameterizedType> exceptions = new ArrayList<>();
+
+    private final BytecodeBlock body;
+    private String comment;
+
+    public MethodDefinition(
+        ClassDefinition declaringClass,
+        EnumSet<Access> access,
+        String name,
+        ParameterizedType returnType,
+        Parameter... parameters) {
+        this(declaringClass, access, name, returnType, List.of(parameters));
+    }
+
+    public MethodDefinition(
+        ClassDefinition declaringClass,
+        EnumSet<Access> access,
+        String name,
+        ParameterizedType returnType,
+        Collection<Parameter> parameters) {
+        checkArgument(parameters.size() <= 254, "Too many parameters for method");
+
+        this.declaringClass = declaringClass;
+        body = new BytecodeBlock();
+
+        this.access = access;
+        this.name = name;
+        this.returnType = returnType != null ? returnType : type(void.class);
+        this.parameters = List.copyOf(parameters);
+        this.parameterTypes = parameters.stream().map(Parameter::getType).collect(Collectors.toList());
+        this.parameterAnnotations = parameters.stream().map(p -> new ArrayList<AnnotationDefinition>()).collect(Collectors.toList());
+        Optional<ParameterizedType> thisType = Optional.empty();
+        if (!declaringClass.isInterface() && !access.contains(STATIC)) {
+            thisType = Optional.of(declaringClass.getType());
+        }
+        scope = new Scope(thisType, parameters);
+    }
+
+    public ClassDefinition getDeclaringClass() {
+        return declaringClass;
+    }
+
+    public List<AnnotationDefinition> getAnnotations() {
+        return List.copyOf(annotations);
+    }
+
+    public List<AnnotationDefinition> getParameterAnnotations(int index) {
+        return List.copyOf(parameterAnnotations.get(index));
+    }
+
+    public EnumSet<Access> getAccess() {
+        return access;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public ParameterizedType getReturnType() {
+        return returnType;
+    }
+
+    public List<Parameter> getParameters() {
+        return parameters;
+    }
+
+    public List<ParameterizedType> getParameterTypes() {
+        return parameterTypes;
+    }
+
+    public List<ParameterizedType> getExceptions() {
+        return exceptions;
+    }
+
+    public MethodDefinition addException(Class<? extends Throwable> exceptionClass) {
+        exceptions.add(type(exceptionClass));
+        return this;
+    }
+
+    public MethodDefinition comment(String format, Object... args) {
+        this.comment = String.format(format, args);
+        return this;
+    }
+
+    public String getComment() {
+        return comment;
+    }
+
+    public Scope getScope() {
+        return scope;
+    }
+
+    public Variable getThis() {
+        return scope.getThis();
+    }
+
+    public String getMethodDescriptor() {
+        return methodDescription(returnType, parameterTypes);
+    }
+
+    public BytecodeBlock getBody() {
+        if (declaringClass.isInterface()) {
+            throw new IllegalAccessError("Interface does not have method body");
+        }
+        return body;
+    }
+
+    public AnnotationDefinition declareAnnotation(Class<?> type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public AnnotationDefinition declareAnnotation(ParameterizedType type) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        annotations.add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public AnnotationDefinition declareParameterAnnotation(Class<?> type, int parameterIndex) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        parameterAnnotations.get(parameterIndex).add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public AnnotationDefinition declareParameterAnnotation(ParameterizedType type, int parameterIndex) {
+        AnnotationDefinition annotationDefinition = new AnnotationDefinition(type);
+        parameterAnnotations.get(parameterIndex).add(annotationDefinition);
+        return annotationDefinition;
+    }
+
+    public void visit(ClassVisitor visitor) {
+        visit(visitor, false);
+    }
+
+    public void visit(ClassVisitor visitor, boolean addReturn) {
+        String[] exceptions = new String[this.exceptions.size()];
+        for (int i = 0; i < exceptions.length; i++) {
+            exceptions[i] = this.exceptions.get(i).getClassName();
+        }
+
+        MethodVisitor methodVisitor = visitor.visitMethod(toAccessModifier(access),
+            name,
+            getMethodDescriptor(),
+            genericMethodSignature(returnType, parameterTypes),
+            exceptions);
+
+        if (methodVisitor == null) {
+            return;
+        }
+
+        // visit method annotations
+        for (AnnotationDefinition annotation : annotations) {
+            annotation.visitMethodAnnotation(methodVisitor);
+        }
+
+        // visit parameter annotations
+        for (int parameterIndex = 0; parameterIndex < parameterAnnotations.size(); parameterIndex++) {
+            List<AnnotationDefinition> parameterAnnotations1 = this.parameterAnnotations.get(parameterIndex);
+            for (AnnotationDefinition parameterAnnotation : parameterAnnotations1) {
+                parameterAnnotation.visitParameterAnnotation(parameterIndex, methodVisitor);
+            }
+        }
+        if (!declaringClass.isInterface()) {
+            // visit code
+            methodVisitor.visitCode();
+
+            // visit instructions
+            MethodGenerationContext generationContext = new MethodGenerationContext(methodVisitor);
+            generationContext.enterScope(scope);
+            body.accept(methodVisitor, generationContext);
+            if (addReturn) {
+                new InsnNode(RETURN).accept(methodVisitor);
+            }
+            generationContext.exitScope(scope);
+        }
+        // done
+        methodVisitor.visitMaxs(-1, -1);
+        methodVisitor.visitEnd();
+    }
+
+    public String toSourceString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append(access.stream().map(Access::toString).collect(Collectors.joining(" ")))
+            .append(' ');
+        sb.append(returnType.getJavaClassName()).append(' ');
+        sb.append(name).append('(');
+        sb.append(parameters.stream().map(Parameter::getSourceString).collect(Collectors.joining(", ")))
+            .append(')');
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        return toSourceString();
+    }
+
+    public static String methodDescription(Class<?> returnType, Class<?>... parameterTypes) {
+        return methodDescription(returnType, List.of(parameterTypes));
+    }
+
+    public static String methodDescription(Class<?> returnType, List<Class<?>> parameterTypes) {
+        return methodDescription(
+            type(returnType),
+            parameterTypes.stream().map(ParameterizedType::type).collect(Collectors.toList()));
+    }
+
+    public static String methodDescription(
+        ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        return methodDescription(returnType, List.of(parameterTypes));
+    }
+
+    public static String methodDescription(
+        ParameterizedType returnType,
+        List<ParameterizedType> parameterTypes) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("(");
+        sb.append(parameterTypes.stream().map(ParameterizedType::getType).collect(Collectors.joining("")));
+        sb.append(")");
+        sb.append(returnType.getType());
+        return sb.toString();
+    }
+
+    public static String genericMethodSignature(
+        ParameterizedType returnType,
+        ParameterizedType... parameterTypes) {
+        return genericMethodSignature(returnType, List.of(parameterTypes));
+    }
+
+    public static String genericMethodSignature(
+        ParameterizedType returnType,
+        List<ParameterizedType> parameterTypes) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("(");
+        sb.append(parameterTypes.stream().map(ParameterizedType::toString).collect(Collectors.joining("")));
+        sb.append(")");
+        sb.append(returnType);
+        return sb.toString();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodGenerationContext.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodGenerationContext.java
new file mode 100644
index 0000000..e62da9d
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/MethodGenerationContext.java
@@ -0,0 +1,122 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.debug.LocalVariableNode;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.ArrayDeque;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+public class MethodGenerationContext {
+    private final MethodVisitor methodVisitor;
+
+    private final Set<Scope> allEnteredScopes = new LinkedHashSet<>();
+    private final Deque<ScopeContext> scopes = new ArrayDeque<>();
+
+    private final Map<Variable, Integer> variableSlots = new HashMap<>();
+    private int nextSlot;
+
+    private int currentLineNumber = -1;
+
+    public MethodGenerationContext(MethodVisitor methodVisitor) {
+        this.methodVisitor = requireNonNull(methodVisitor, "methodVisitor is null");
+    }
+
+    public void enterScope(Scope scope) {
+        requireNonNull(scope, "scope is null");
+
+        checkArgument(!allEnteredScopes.contains(scope), "scope has already been entered");
+        allEnteredScopes.add(scope);
+
+        ScopeContext scopeContext = new ScopeContext(scope);
+        scopes.addLast(scopeContext);
+
+        for (Variable variable : scopeContext.getVariables()) {
+            checkArgument(!"this".equals(variable.getName()) || nextSlot == 0, "The 'this' variable must be in slot 0");
+            variableSlots.put(variable, nextSlot);
+            nextSlot += Type.getType(variable.getType().getType()).getSize();
+        }
+
+        scopeContext.getStartLabel().accept(methodVisitor, this);
+    }
+
+    public void exitScope(Scope scope) {
+        checkArgument(allEnteredScopes.contains(scope), "scope has not been entered");
+        checkArgument(!scopes.isEmpty() && scope == scopes.peekLast().getScope(), "Scope is not top of the stack");
+
+        ScopeContext scopeContext = scopes.removeLast();
+
+        scopeContext.getEndLabel().accept(methodVisitor, this);
+
+        for (Variable variable : scopeContext.getVariables()) {
+            new LocalVariableNode(variable, scopeContext.getStartLabel(), scopeContext.getEndLabel()).accept(methodVisitor, this);
+        }
+
+        variableSlots.keySet().removeAll(scopeContext.getVariables());
+    }
+
+    public int getVariableSlot(Variable variable) {
+        Integer slot = variableSlots.get(variable);
+        checkArgument(slot != null, "Variable '%s' has not been assigned a slot", variable);
+        return slot;
+    }
+
+    public boolean updateLineNumber(int lineNumber) {
+        if (lineNumber == currentLineNumber) {
+            return false;
+        }
+
+        currentLineNumber = lineNumber;
+        return true;
+    }
+
+    private static final class ScopeContext {
+        private final Scope scope;
+        private final List<Variable> variables;
+
+        private final LabelNode startLabel = new LabelNode("VariableStart");
+        private final LabelNode endLabel = new LabelNode("VariableEnd");
+
+        ScopeContext(Scope scope) {
+            this.scope = scope;
+            this.variables = List.copyOf(scope.getVariables());
+        }
+
+        public Scope getScope() {
+            return scope;
+        }
+
+        public List<Variable> getVariables() {
+            return variables;
+        }
+
+        public LabelNode getStartLabel() {
+            return startLabel;
+        }
+
+        public LabelNode getEndLabel() {
+            return endLabel;
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/OpCode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/OpCode.java
new file mode 100644
index 0000000..5ca541a
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/OpCode.java
@@ -0,0 +1,268 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.instruction.InstructionNode;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+
+@SuppressWarnings("SpellCheckingInspection")
+public enum OpCode
+    implements InstructionNode {
+    NOP(0),
+    ACONST_NULL(1),
+    ICONST_M1(2),
+    ICONST_0(3),
+    ICONST_1(4),
+    ICONST_2(5),
+    ICONST_3(6),
+    ICONST_4(7),
+    ICONST_5(8),
+    LCONST_0(9),
+    LCONST_1(10),
+    FCONST_0(11),
+    FCONST_1(12),
+    FCONST_2(13),
+    DCONST_0(14),
+    DCONST_1(15),
+    BIPUSH(16),
+    SIPUSH(17),
+    LDC(18),
+    LDC_W(19),
+    LDC2_W(20),
+    ILOAD(21),
+    LLOAD(22),
+    FLOAD(23),
+    DLOAD(24),
+    ALOAD(25),
+    ILOAD_0(26),
+    ILOAD_1(27),
+    ILOAD_2(28),
+    ILOAD_3(29),
+    LLOAD_0(30),
+    LLOAD_1(31),
+    LLOAD_2(32),
+    LLOAD_3(33),
+    FLOAD_0(34),
+    FLOAD_1(35),
+    FLOAD_2(36),
+    FLOAD_3(37),
+    DLOAD_0(38),
+    DLOAD_1(39),
+    DLOAD_2(40),
+    DLOAD_3(41),
+    ALOAD_0(42),
+    ALOAD_1(43),
+    ALOAD_2(44),
+    ALOAD_3(45),
+    IALOAD(46),
+    LALOAD(47),
+    FALOAD(48),
+    DALOAD(49),
+    AALOAD(50),
+    BALOAD(51),
+    CALOAD(52),
+    SALOAD(53),
+    ISTORE(54),
+    LSTORE(55),
+    FSTORE(56),
+    DSTORE(57),
+    ASTORE(58),
+    ISTORE_0(59),
+    ISTORE_1(60),
+    ISTORE_2(61),
+    ISTORE_3(62),
+    LSTORE_0(63),
+    LSTORE_1(64),
+    LSTORE_2(65),
+    LSTORE_3(66),
+    FSTORE_0(67),
+    FSTORE_1(68),
+    FSTORE_2(69),
+    FSTORE_3(70),
+    DSTORE_0(71),
+    DSTORE_1(72),
+    DSTORE_2(73),
+    DSTORE_3(74),
+    ASTORE_0(75),
+    ASTORE_1(76),
+    ASTORE_2(77),
+    ASTORE_3(78),
+    IASTORE(79),
+    LASTORE(80),
+    FASTORE(81),
+    DASTORE(82),
+    AASTORE(83),
+    BASTORE(84),
+    CASTORE(85),
+    SASTORE(86),
+    POP(87),
+    POP2(88),
+    DUP(89),
+    DUP_X1(90),
+    DUP_X2(91),
+    DUP2(92),
+    DUP2_X1(93),
+    DUP2_X2(94),
+    SWAP(95),
+    IADD(96),
+    LADD(97),
+    FADD(98),
+    DADD(99),
+    ISUB(100),
+    LSUB(101),
+    FSUB(102),
+    DSUB(103),
+    IMUL(104),
+    LMUL(105),
+    FMUL(106),
+    DMUL(107),
+    IDIV(108),
+    LDIV(109),
+    FDIV(110),
+    DDIV(111),
+    IREM(112),
+    LREM(113),
+    FREM(114),
+    DREM(115),
+    INEG(116),
+    LNEG(117),
+    FNEG(118),
+    DNEG(119),
+    ISHL(120),
+    LSHL(121),
+    ISHR(122),
+    LSHR(123),
+    IUSHR(124),
+    LUSHR(125),
+    IAND(126),
+    LAND(127),
+    IOR(128),
+    LOR(129),
+    IXOR(130),
+    LXOR(131),
+    IINC(132),
+    I2L(133),
+    I2F(134),
+    I2D(135),
+    L2I(136),
+    L2F(137),
+    L2D(138),
+    F2I(139),
+    F2L(140),
+    F2D(141),
+    D2I(142),
+    D2L(143),
+    D2F(144),
+    I2B(145),
+    I2C(146),
+    I2S(147),
+    LCMP(148),
+    FCMPL(149),
+    FCMPG(150),
+    DCMPL(151),
+    DCMPG(152),
+    IFEQ(153),
+    IFNE(154),
+    IFLT(155),
+    IFGE(156),
+    IFGT(157),
+    IFLE(158),
+    IF_ICMPEQ(159),
+    IF_ICMPNE(160),
+    IF_ICMPLT(161),
+    IF_ICMPGE(162),
+    IF_ICMPGT(163),
+    IF_ICMPLE(164),
+    IF_ACMPEQ(165),
+    IF_ACMPNE(166),
+    GOTO(167),
+    JSR(168),
+    RET(169),
+    TABLESWITCH(170),
+    LOOKUPSWITCH(171),
+    IRETURN(172),
+    LRETURN(173),
+    FRETURN(174),
+    DRETURN(175),
+    ARETURN(176),
+    RETURN(177),
+    GETSTATIC(178),
+    PUTSTATIC(179),
+    GETFIELD(180),
+    PUTFIELD(181),
+    INVOKEVIRTUAL(182),
+    INVOKESPECIAL(183),
+    INVOKESTATIC(184),
+    INVOKEINTERFACE(185),
+    INVOKEDYNAMIC(186),
+    NEW(187),
+    NEWARRAY(188),
+    ANEWARRAY(189),
+    ARRAYLENGTH(190),
+    ATHROW(191),
+    CHECKCAST(192),
+    INSTANCEOF(193),
+    MONITORENTER(194),
+    MONITOREXIT(195),
+    WIDE(196),
+    MULTIANEWARRAY(197),
+    IFNULL(198),
+    IFNONNULL(199),
+    GOTO_W(200),
+    JSR_W(201);
+
+    private static final Map<Integer, OpCode> OP_CODE_INDEX;
+
+    static {
+        OP_CODE_INDEX = Arrays.stream(values()).collect(Collectors.toMap(OpCode::getOpCode, Function.identity()));
+    }
+
+    public static OpCode getOpCode(int opCode) {
+        OpCode value = OP_CODE_INDEX.get(opCode);
+        checkArgument(value != null, "Unknown opCode %s", opCode);
+        return value;
+    }
+
+    private final int opCode;
+
+    OpCode(int opCode) {
+        this.opCode = opCode;
+    }
+
+    public int getOpCode() {
+        return opCode;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        visitor.visitInsn(opCode);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitInstruction(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Parameter.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Parameter.java
new file mode 100644
index 0000000..a87220f
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Parameter.java
@@ -0,0 +1,33 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+public class Parameter
+    extends Variable {
+    public static Parameter arg(String name, Class<?> type) {
+        return new Parameter(name, ParameterizedType.type(type));
+    }
+
+    public static Parameter arg(String name, ParameterizedType type) {
+        return new Parameter(name, type);
+    }
+
+    Parameter(String name, ParameterizedType type) {
+        super(name, type);
+    }
+
+    String getSourceString() {
+        return getType().getJavaClassName() + " " + getName();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ParameterizedType.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ParameterizedType.java
new file mode 100644
index 0000000..9092e36
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/ParameterizedType.java
@@ -0,0 +1,285 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+import org.objectweb.asm.Type;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+public class ParameterizedType {
+    public static ParameterizedType typeFromJavaClassName(String className) {
+        requireNonNull(className, "type is null");
+        return new ParameterizedType(className.replace('.', '/'));
+    }
+
+    public static ParameterizedType typeFromPathName(String className) {
+        requireNonNull(className, "type is null");
+        return new ParameterizedType(className);
+    }
+
+    public static ParameterizedType type(Type type) {
+        requireNonNull(type, "type is null");
+        return new ParameterizedType(type.getInternalName());
+    }
+
+    public static ParameterizedType type(Class<?> type) {
+        requireNonNull(type, "type is null");
+        return new ParameterizedType(type);
+    }
+
+    public static ParameterizedType type(Class<?> type, Class<?>... parameters) {
+        requireNonNull(type, "type is null");
+        return new ParameterizedType(type, parameters);
+    }
+
+    public static ParameterizedType type(Class<?> type, ParameterizedType... parameters) {
+        requireNonNull(type, "type is null");
+        return new ParameterizedType(type, parameters);
+    }
+
+    private final String type;
+    private final String className;
+    private final String simpleName;
+    private final List<String> parameters;
+
+    private final boolean isInterface;
+    @Nullable
+    private final Class<?> primitiveType;
+    @Nullable
+    private final ParameterizedType arrayComponentType;
+
+    public ParameterizedType(String className) {
+        requireNonNull(className, "className is null");
+        checkArgument(!className.contains("."), "Invalid class name %s", className);
+        checkArgument(!className.endsWith(";"), "Invalid class name %s", className);
+
+        this.className = className;
+        this.simpleName = className.substring(className.lastIndexOf("/") + 1);
+        this.type = "L" + className + ";";
+        this.parameters = List.of();
+
+        this.isInterface = false;
+        this.primitiveType = null;
+        this.arrayComponentType = null;
+    }
+
+    private ParameterizedType(Class<?> type) {
+        requireNonNull(type, "type is null");
+        this.type = toInternalIdentifier(type);
+        this.className = getPathName(type);
+        this.simpleName = type.getSimpleName();
+        this.parameters = List.of();
+
+        this.isInterface = type.isInterface();
+        this.primitiveType = type.isPrimitive() ? type : null;
+        this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null;
+    }
+
+    private ParameterizedType(Class<?> type, Class<?>... parameters) {
+        requireNonNull(type, "type is null");
+        this.type = toInternalIdentifier(type);
+        this.className = getPathName(type);
+        this.simpleName = type.getSimpleName();
+
+        this.parameters = Arrays.stream(parameters).map(ParameterizedType::toInternalIdentifier).collect(Collectors.toList());
+
+        this.isInterface = type.isInterface();
+        this.primitiveType = type.isPrimitive() ? type : null;
+        this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null;
+    }
+
+    private ParameterizedType(Class<?> type, ParameterizedType... parameters) {
+        requireNonNull(type, "type is null");
+        this.type = toInternalIdentifier(type);
+        this.className = getPathName(type);
+        this.simpleName = type.getSimpleName();
+
+        this.parameters = Arrays.stream(parameters).map(ParameterizedType::toString).collect(Collectors.toList());
+
+        this.isInterface = type.isInterface();
+        this.primitiveType = type.isPrimitive() ? type : null;
+        this.arrayComponentType = type.isArray() ? type(type.getComponentType()) : null;
+    }
+
+    public String getClassName() {
+        return className;
+    }
+
+    public String getJavaClassName() {
+        return className.replace('/', '.');
+    }
+
+    public String getSimpleName() {
+        return simpleName;
+    }
+
+    public String getType() {
+        return type;
+    }
+
+    public Type getAsmType() {
+        return Type.getObjectType(className);
+    }
+
+    public String getGenericSignature() {
+        StringBuilder sb = new StringBuilder();
+        if (primitiveType != null || arrayComponentType != null) {
+            return type;
+        }
+        sb.append('L').append(className);
+        if (!parameters.isEmpty()) {
+            sb.append("<");
+            for (String parameterType : parameters) {
+                sb.append(parameterType);
+            }
+            sb.append(">");
+        }
+        sb.append(";");
+        return sb.toString();
+    }
+
+    public boolean isGeneric() {
+        return !parameters.isEmpty();
+    }
+
+    public boolean isInterface() {
+        return isInterface;
+    }
+
+    @Nullable
+    public Class<?> getPrimitiveType() {
+        return primitiveType;
+    }
+
+    public boolean isPrimitive() {
+        return primitiveType != null;
+    }
+
+    @Nullable
+    public ParameterizedType getArrayComponentType() {
+        return arrayComponentType;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+
+        ParameterizedType that = (ParameterizedType)o;
+
+        if (!type.equals(that.type)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return type.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return getGenericSignature();
+    }
+
+    public static String getPathName(Class<?> n) {
+        return n.getName().replace('.', '/');
+    }
+
+    private static String toInternalIdentifier(Class<?> n) {
+        if (n.isArray()) {
+            n = n.getComponentType();
+            if (n.isPrimitive()) {
+                if (n == Byte.TYPE) {
+                    return "[B";
+                }
+                else if (n == Boolean.TYPE) {
+                    return "[Z";
+                }
+                else if (n == Short.TYPE) {
+                    return "[S";
+                }
+                else if (n == Character.TYPE) {
+                    return "[C";
+                }
+                else if (n == Integer.TYPE) {
+                    return "[I";
+                }
+                else if (n == Float.TYPE) {
+                    return "[F";
+                }
+                else if (n == Double.TYPE) {
+                    return "[D";
+                }
+                else if (n == Long.TYPE) {
+                    return "[J";
+                }
+                else {
+                    throw new RuntimeException("Unrecognized type in compiler: " + n.getName());
+                }
+            }
+            else {
+                return "[" + toInternalIdentifier(n);
+            }
+        }
+        else {
+            if (n.isPrimitive()) {
+                if (n == Byte.TYPE) {
+                    return "B";
+                }
+                else if (n == Boolean.TYPE) {
+                    return "Z";
+                }
+                else if (n == Short.TYPE) {
+                    return "S";
+                }
+                else if (n == Character.TYPE) {
+                    return "C";
+                }
+                else if (n == Integer.TYPE) {
+                    return "I";
+                }
+                else if (n == Float.TYPE) {
+                    return "F";
+                }
+                else if (n == Double.TYPE) {
+                    return "D";
+                }
+                else if (n == Long.TYPE) {
+                    return "J";
+                }
+                else if (n == Void.TYPE) {
+                    return "V";
+                }
+                else {
+                    throw new RuntimeException("Unrecognized type in compiler: " + n.getName());
+                }
+            }
+            else {
+                return "L" + getPathName(n) + ";";
+            }
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Scope.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Scope.java
new file mode 100644
index 0000000..46c7060
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Scope.java
@@ -0,0 +1,102 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.expression.BytecodeExpression;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.TreeMap;
+import org.objectweb.asm.Type;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+public class Scope {
+    private final Map<String, Variable> variables = new TreeMap<>();
+    private final List<Variable> allVariables = new ArrayList<>();
+
+    private final Variable thisVariable;
+
+    private int nextTempVariableId;
+
+    // This can only be constructed by a method definition
+    Scope(Optional<ParameterizedType> thisType, Iterable<Parameter> parameters) {
+        if (thisType.isPresent()) {
+            thisVariable = new Variable("this", thisType.get());
+            variables.put("this", thisVariable);
+            allVariables.add(thisVariable);
+        }
+        else {
+            thisVariable = null;
+        }
+
+        for (Parameter parameter : parameters) {
+            variables.put(parameter.getName(), parameter);
+            allVariables.add(parameter);
+        }
+    }
+
+    public List<Variable> getVariables() {
+        return List.copyOf(allVariables);
+    }
+
+    public Variable createTempVariable(Class<?> type) {
+        // reserve a slot for this variable
+        Variable variable = new Variable("temp_" + nextTempVariableId, type(type));
+        nextTempVariableId += Type.getType(type(type).getType()).getSize();
+
+        allVariables.add(variable);
+
+        return variable;
+    }
+
+    public Variable getThis() {
+        checkState(thisVariable != null, "Static methods do not have a 'this' variable");
+        return thisVariable;
+    }
+
+    public Variable getVariable(String name) {
+        Variable variable = variables.get(name);
+        checkArgument(variable != null, "Variable %s not defined", name);
+        return variable;
+    }
+
+    public Variable declareVariable(Class<?> type, String variableName) {
+        return declareVariable(type(type), variableName);
+    }
+
+    public Variable declareVariable(ParameterizedType type, String variableName) {
+        requireNonNull(type, "type is null");
+        requireNonNull(variableName, "variableName is null");
+        checkArgument(!variables.containsKey(variableName), "There is already a variable named %s", variableName);
+        checkArgument(!"this".equals(variableName), "The 'this' variable can not be declared");
+
+        Variable variable = new Variable(variableName, type);
+
+        variables.put(variableName, variable);
+        allVariables.add(variable);
+
+        return variable;
+    }
+
+    public Variable declareVariable(String variableName, BytecodeBlock block, BytecodeExpression initialValue) {
+        Variable variable = declareVariable(initialValue.getType(), variableName);
+        block.append(variable.set(initialValue));
+        return variable;
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/SmartClassWriter.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/SmartClassWriter.java
new file mode 100644
index 0000000..12eda2f
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/SmartClassWriter.java
@@ -0,0 +1,54 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import org.objectweb.asm.ClassWriter;
+
+import static com.facebook.presto.bytecode.ParameterizedType.typeFromPathName;
+
+public class SmartClassWriter
+        extends ClassWriter
+{
+    private final ClassInfoLoader classInfoLoader;
+
+    public SmartClassWriter(ClassInfoLoader classInfoLoader)
+    {
+        super(ClassWriter.COMPUTE_FRAMES);
+        this.classInfoLoader = classInfoLoader;
+    }
+
+    @Override
+    protected String getCommonSuperClass(String aType, String bType)
+    {
+        ClassInfo aClassInfo = classInfoLoader.loadClassInfo(typeFromPathName(aType));
+        ClassInfo bClassInfo = classInfoLoader.loadClassInfo(typeFromPathName(bType));
+
+        if (aClassInfo.isAssignableFrom(bClassInfo)) {
+            return aType;
+        }
+        if (bClassInfo.isAssignableFrom(aClassInfo)) {
+            return bType;
+        }
+        if (aClassInfo.isInterface() || bClassInfo.isInterface()) {
+            return "java/lang/Object";
+        }
+        else {
+            do {
+                aClassInfo = aClassInfo.getSuperclass();
+            }
+            while (!aClassInfo.isAssignableFrom(bClassInfo));
+            return aClassInfo.getType().getClassName();
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Variable.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Variable.java
new file mode 100644
index 0000000..fb46c2e
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/Variable.java
@@ -0,0 +1,89 @@
+/*
+ * 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.facebook.presto.bytecode;
+
+import com.facebook.presto.bytecode.expression.BytecodeExpression;
+import com.facebook.presto.bytecode.instruction.VariableInstruction;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.expression.BytecodeExpressions.add;
+import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantInt;
+import static java.util.Objects.requireNonNull;
+
+public class Variable
+    extends BytecodeExpression {
+    private final String name;
+
+    public Variable(String name, ParameterizedType type) {
+        super(type);
+        this.name = requireNonNull(name, "name is null");
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public BytecodeExpression set(BytecodeExpression value) {
+        return new SetVariableBytecodeExpression(this, value);
+    }
+
+    public BytecodeExpression increment() {
+        return new SetVariableBytecodeExpression(this, add(this, constantInt(1)));
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        return VariableInstruction.loadVariable(this);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return name;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    private static final class SetVariableBytecodeExpression
+        extends BytecodeExpression {
+        private final Variable variable;
+        private final BytecodeExpression value;
+
+        SetVariableBytecodeExpression(Variable variable, BytecodeExpression value) {
+            super(type(void.class));
+            this.variable = requireNonNull(variable, "variable is null");
+            this.value = requireNonNull(value, "value is null");
+        }
+
+        @Override
+        public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+            return new BytecodeBlock()
+                .append(value)
+                .putVariable(variable);
+        }
+
+        @Override
+        public List<BytecodeNode> getChildNodes() {
+            return List.of(value);
+        }
+
+        @Override
+        protected String formatOneLine() {
+            return variable.getName() + " = " + value;
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/CaseStatement.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/CaseStatement.java
new file mode 100644
index 0000000..dafe6d7
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/CaseStatement.java
@@ -0,0 +1,72 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.Objects;
+
+import static java.util.Objects.requireNonNull;
+
+public class CaseStatement
+    implements Comparable<CaseStatement> {
+    private final int key;
+    private final BytecodeNode body;
+    private final LabelNode label;
+
+    CaseStatement(int key, BytecodeNode body, LabelNode label) {
+        this.key = key;
+        this.body = requireNonNull(body, "body is null");
+        this.label = requireNonNull(label, "label is null");
+    }
+
+    public int getKey() {
+        return key;
+    }
+
+    public BytecodeNode getBody() {
+        return body;
+    }
+
+    public LabelNode getLabel() {
+        return label;
+    }
+
+    @Override
+    public int compareTo(CaseStatement o) {
+        return Integer.compare(key, o.key);
+    }
+
+    @Override
+    public int hashCode() {
+        return key;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        CaseStatement other = (CaseStatement)obj;
+        return Objects.equals(this.key, other.key);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "{key=" + key + '}';
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/DoWhileLoop.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/DoWhileLoop.java
new file mode 100644
index 0000000..9e4046d
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/DoWhileLoop.java
@@ -0,0 +1,106 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+
+public class DoWhileLoop
+    implements FlowControl {
+    private final String comment;
+    private final BytecodeBlock body = new BytecodeBlock();
+    private final BytecodeBlock condition = new BytecodeBlock();
+
+    private final LabelNode beginLabel = new LabelNode("begin");
+    private final LabelNode continueLabel = new LabelNode("continue");
+    private final LabelNode endLabel = new LabelNode("end");
+
+    public DoWhileLoop() {
+        this.comment = null;
+    }
+
+    public DoWhileLoop(String format, Object... args) {
+        this.comment = String.format(format, args);
+    }
+
+    @Override
+    public String getComment() {
+        return comment;
+    }
+
+    public LabelNode getContinueLabel() {
+        return continueLabel;
+    }
+
+    public LabelNode getEndLabel() {
+        return endLabel;
+    }
+
+    public BytecodeBlock body() {
+        return body;
+    }
+
+    public DoWhileLoop body(BytecodeNode node) {
+        checkState(body.isEmpty(), "body already set");
+        body.append(node);
+        return this;
+    }
+
+    public BytecodeBlock condition() {
+        return condition;
+    }
+
+    public DoWhileLoop condition(BytecodeNode node) {
+        checkState(condition.isEmpty(), "condition already set");
+        condition.append(node);
+        return this;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        checkState(!condition.isEmpty(), "DoWhileLoop does not have a condition set");
+
+        BytecodeBlock block = new BytecodeBlock()
+            .visitLabel(beginLabel)
+            .append(new BytecodeBlock()
+                .setDescription("body")
+                .append(body))
+            .visitLabel(continueLabel)
+            .append(new BytecodeBlock()
+                .setDescription("condition")
+                .append(condition))
+            .ifFalseGoto(endLabel)
+            .gotoLabel(beginLabel)
+            .visitLabel(endLabel);
+
+        block.accept(visitor, generationContext);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(body, condition);
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitDoWhile(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/FlowControl.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/FlowControl.java
new file mode 100644
index 0000000..013d997
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/FlowControl.java
@@ -0,0 +1,22 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+
+public interface FlowControl
+        extends BytecodeNode
+{
+    String getComment();
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/ForLoop.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/ForLoop.java
new file mode 100644
index 0000000..cdbd4c8
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/ForLoop.java
@@ -0,0 +1,155 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+
+public class ForLoop
+        implements FlowControl
+{
+    private final String comment;
+    private final BytecodeBlock initialize = new BytecodeBlock();
+    private final BytecodeBlock condition = new BytecodeBlock();
+    private final BytecodeBlock update = new BytecodeBlock();
+    private final BytecodeBlock body = new BytecodeBlock();
+
+    private final LabelNode beginLabel = new LabelNode("beginLabel");
+    private final LabelNode continueLabel = new LabelNode("continue");
+    private final LabelNode endLabel = new LabelNode("end");
+
+    public ForLoop()
+    {
+        this.comment = null;
+    }
+
+    public ForLoop(String format, Object... args)
+    {
+        this.comment = String.format(format, args);
+    }
+
+    @Override
+    public String getComment()
+    {
+        return comment;
+    }
+
+    public LabelNode getContinueLabel()
+    {
+        return continueLabel;
+    }
+
+    public LabelNode getEndLabel()
+    {
+        return endLabel;
+    }
+
+    public BytecodeBlock initialize()
+    {
+        return initialize;
+    }
+
+    public ForLoop initialize(BytecodeNode node)
+    {
+        checkState(initialize.isEmpty(), "initialize already set");
+        initialize.append(node);
+        return this;
+    }
+
+    public BytecodeBlock condition()
+    {
+        return condition;
+    }
+
+    public ForLoop condition(BytecodeNode node)
+    {
+        checkState(condition.isEmpty(), "condition already set");
+        condition.append(node);
+        return this;
+    }
+
+    public BytecodeBlock update()
+    {
+        return update;
+    }
+
+    public ForLoop update(BytecodeNode node)
+    {
+        checkState(update.isEmpty(), "update already set");
+        update.append(node);
+        return this;
+    }
+
+    public BytecodeBlock body()
+    {
+        return body;
+    }
+
+    public ForLoop body(BytecodeNode node)
+    {
+        checkState(body.isEmpty(), "body already set");
+        body.append(node);
+        return this;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext)
+    {
+        checkState(!condition.isEmpty(), "ForLoop does not have a condition set");
+
+        BytecodeBlock block = new BytecodeBlock();
+
+        block.append(new BytecodeBlock()
+                .setDescription("initialize")
+                .append(initialize));
+
+        block.visitLabel(beginLabel)
+                .append(new BytecodeBlock()
+                        .setDescription("condition")
+                        .append(condition))
+                .ifFalseGoto(endLabel);
+
+        block.append(new BytecodeBlock()
+                .setDescription("body")
+                .append(body));
+
+        block.visitLabel(continueLabel)
+                .append(new BytecodeBlock()
+                        .setDescription("update")
+                        .append(update))
+                .gotoLabel(beginLabel)
+                .visitLabel(endLabel);
+
+        block.accept(visitor, generationContext);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(initialize, condition, update, body);
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor)
+    {
+        return visitor.visitFor(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/IfStatement.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/IfStatement.java
new file mode 100644
index 0000000..3fbdd4b
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/IfStatement.java
@@ -0,0 +1,124 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+
+public class IfStatement
+    implements FlowControl {
+    private final String comment;
+    private final BytecodeBlock condition = new BytecodeBlock();
+    private final BytecodeBlock ifTrue = new BytecodeBlock();
+    private final BytecodeBlock ifFalse = new BytecodeBlock();
+
+    private final LabelNode falseLabel = new LabelNode("false");
+    private final LabelNode outLabel = new LabelNode("out");
+
+    public IfStatement() {
+        this.comment = null;
+    }
+
+    public IfStatement(String format, Object... args) {
+        this.comment = String.format(format, args);
+    }
+
+    @Override
+    public String getComment() {
+        return comment;
+    }
+
+    public BytecodeBlock condition() {
+        return condition;
+    }
+
+    public IfStatement condition(BytecodeNode node) {
+        checkState(condition.isEmpty(), "condition already set");
+        condition.append(node);
+        return this;
+    }
+
+    public BytecodeBlock ifTrue() {
+        return ifTrue;
+    }
+
+    public IfStatement ifTrue(BytecodeNode node) {
+        checkState(ifTrue.isEmpty(), "ifTrue already set");
+        ifTrue.append(node);
+        return this;
+    }
+
+    public BytecodeBlock ifFalse() {
+        return ifFalse;
+    }
+
+    public IfStatement ifFalse(BytecodeNode node) {
+        checkState(ifFalse.isEmpty(), "ifFalse already set");
+        ifFalse.append(node);
+        return this;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        checkState(!condition.isEmpty(), "IfStatement does not have a condition set");
+        checkState(!ifTrue.isEmpty() || !ifFalse.isEmpty(), "IfStatement does not have a true or false block set");
+
+        BytecodeBlock block = new BytecodeBlock();
+
+        // if !condition goto false;
+        block.append(new BytecodeBlock()
+            .setDescription("condition")
+            .append(condition));
+        block.ifFalseGoto(falseLabel);
+
+        if (!ifTrue.isEmpty()) {
+            block.append(new BytecodeBlock()
+                .setDescription("ifTrue")
+                .append(ifTrue));
+        }
+
+        if (!ifFalse.isEmpty()) {
+            // close true case by skipping to end
+            block.gotoLabel(outLabel);
+
+            block.visitLabel(falseLabel);
+            block.append(new BytecodeBlock()
+                .setDescription("ifFalse")
+                .append(ifFalse));
+            block.visitLabel(outLabel);
+        }
+        else {
+            block.visitLabel(falseLabel);
+        }
+
+        block.accept(visitor, generationContext);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(condition, ifTrue, ifFalse);
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitIf(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/SwitchStatement.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/SwitchStatement.java
new file mode 100644
index 0000000..c2f38a9
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/SwitchStatement.java
@@ -0,0 +1,171 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.expression.BytecodeExpression;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+
+public class SwitchStatement
+    implements FlowControl {
+    public static SwitchBuilder switchBuilder() {
+        return new SwitchBuilder();
+    }
+
+    private final LabelNode endLabel = new LabelNode("switchEnd");
+    private final LabelNode defaultLabel = new LabelNode("switchDefault");
+    private final String comment;
+    private final BytecodeExpression expression;
+    private final SortedSet<CaseStatement> cases;
+    private final BytecodeNode defaultBody;
+
+    private SwitchStatement(
+        String comment,
+        BytecodeExpression expression,
+        Collection<CaseStatement> cases,
+        BytecodeNode defaultBody) {
+        this.comment = comment;
+        this.expression = requireNonNull(expression, "expression is null");
+
+        final TreeSet<CaseStatement> sorted = new TreeSet<>(comparing(CaseStatement::getKey));
+        sorted.addAll(cases);
+
+        this.cases = Collections.unmodifiableSortedSet(sorted);
+        this.defaultBody = defaultBody;
+    }
+
+    @Override
+    public String getComment() {
+        return comment;
+    }
+
+    public BytecodeExpression expression() {
+        return expression;
+    }
+
+    public SortedSet<CaseStatement> cases() {
+        return cases;
+    }
+
+    public LabelNode getDefaultLabel() {
+        return defaultLabel;
+    }
+
+    public BytecodeNode getDefaultBody() {
+        return defaultBody;
+    }
+
+    public LabelNode getEndLabel() {
+        return endLabel;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        // build switch table
+        int[] keys = new int[cases.size()];
+        Label[] labels = new Label[cases.size()];
+
+        int index = 0;
+        for (CaseStatement caseStatement : cases) {
+            keys[index] = caseStatement.getKey();
+            labels[index] = caseStatement.getLabel().getLabel();
+            index++;
+        }
+
+        // build case blocks
+        BytecodeBlock block = new BytecodeBlock();
+
+        for (CaseStatement caseStatement : cases) {
+            block.visitLabel(caseStatement.getLabel())
+                .append(caseStatement.getBody())
+                .gotoLabel(endLabel);
+        }
+
+        // build default block
+        block.visitLabel(defaultLabel);
+
+        if (defaultBody != null) {
+            block.append(defaultBody);
+        }
+
+        block.visitLabel(endLabel);
+
+        // emit code
+        expression.accept(visitor, generationContext);
+        visitor.visitLookupSwitchInsn(defaultLabel.getLabel(), keys, labels);
+        block.accept(visitor, generationContext);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitSwitch(parent, this);
+    }
+
+    public static class SwitchBuilder {
+        private final Set<CaseStatement> cases = new HashSet<>();
+        private String comment;
+        private BytecodeExpression expression;
+        private LabelNode defaultLabel;
+        private BytecodeNode defaultBody;
+
+        public SwitchBuilder comment(String format, Object... args) {
+            this.comment = String.format(format, args);
+            return this;
+        }
+
+        public SwitchBuilder expression(BytecodeExpression expression) {
+            this.expression = expression;
+            return this;
+        }
+
+        public SwitchBuilder addCase(int key, BytecodeNode body) {
+            LabelNode label = new LabelNode("switchCase:" + key);
+            CaseStatement statement = new CaseStatement(key, body, label);
+            checkState(cases.add(statement), "case already exists for value [%s]", key);
+            return this;
+        }
+
+        public SwitchBuilder defaultCase(BytecodeNode body) {
+            checkState(defaultBody == null, "default case already set");
+            this.defaultBody = requireNonNull(body, "body is null");
+            return this;
+        }
+
+        public SwitchStatement build() {
+            checkState(expression != null, "expression is not set");
+            return new SwitchStatement(comment, expression, cases, defaultBody);
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/TryCatch.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/TryCatch.java
new file mode 100644
index 0000000..c932494
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/TryCatch.java
@@ -0,0 +1,97 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static java.util.Objects.requireNonNull;
+
+public class TryCatch
+    implements FlowControl {
+    private final String comment;
+    private final BytecodeNode tryNode;
+    private final BytecodeNode catchNode;
+    private final String exceptionName;
+
+    public TryCatch(BytecodeNode tryNode, BytecodeNode catchNode, ParameterizedType exceptionType) {
+        this(null, tryNode, catchNode, exceptionType);
+    }
+
+    public TryCatch(String comment, BytecodeNode tryNode, BytecodeNode catchNode, ParameterizedType exceptionType) {
+        this.comment = comment;
+        this.tryNode = requireNonNull(tryNode, "tryNode is null");
+        this.catchNode = requireNonNull(catchNode, "catchNode is null");
+        this.exceptionName = (exceptionType != null) ? exceptionType.getClassName() : null;
+    }
+
+    @Override
+    public String getComment() {
+        return comment;
+    }
+
+    public BytecodeNode getTryNode() {
+        return tryNode;
+    }
+
+    public BytecodeNode getCatchNode() {
+        return catchNode;
+    }
+
+    public String getExceptionName() {
+        return exceptionName;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        LabelNode tryStart = new LabelNode("tryStart");
+        LabelNode tryEnd = new LabelNode("tryEnd");
+        LabelNode handler = new LabelNode("handler");
+        LabelNode done = new LabelNode("done");
+
+        BytecodeBlock block = new BytecodeBlock();
+
+        // try block
+        block.visitLabel(tryStart)
+            .append(tryNode)
+            .visitLabel(tryEnd)
+            .gotoLabel(done);
+
+        // handler block
+        block.visitLabel(handler)
+            .append(catchNode);
+
+        // all done
+        block.visitLabel(done);
+
+        block.accept(visitor, generationContext);
+        visitor.visitTryCatchBlock(tryStart.getLabel(), tryEnd.getLabel(), handler.getLabel(), exceptionName);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(tryNode, catchNode);
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitTryCatch(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/WhileLoop.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/WhileLoop.java
new file mode 100644
index 0000000..3ad6298
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/control/WhileLoop.java
@@ -0,0 +1,100 @@
+/*
+ * 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.facebook.presto.bytecode.control;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkState;
+
+public class WhileLoop
+    implements FlowControl {
+    private final String comment;
+    private final BytecodeBlock condition = new BytecodeBlock();
+    private final BytecodeBlock body = new BytecodeBlock();
+
+    private final LabelNode continueLabel = new LabelNode("continue");
+    private final LabelNode endLabel = new LabelNode("end");
+
+    public WhileLoop() {
+        this.comment = null;
+    }
+
+    public WhileLoop(String format, Object... args) {
+        this.comment = String.format(format, args);
+    }
+
+    @Override
+    public String getComment() {
+        return comment;
+    }
+
+    public LabelNode getContinueLabel() {
+        return continueLabel;
+    }
+
+    public LabelNode getEndLabel() {
+        return endLabel;
+    }
+
+    public BytecodeBlock condition() {
+        return condition;
+    }
+
+    public WhileLoop condition(BytecodeNode node) {
+        checkState(condition.isEmpty(), "condition already set");
+        condition.append(node);
+        return this;
+    }
+
+    public BytecodeBlock body() {
+        return body;
+    }
+
+    public WhileLoop body(BytecodeNode node) {
+        checkState(body.isEmpty(), "body already set");
+        body.append(node);
+        return this;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        checkState(!condition.isEmpty(), "WhileLoop does not have a condition set");
+
+        BytecodeBlock block = new BytecodeBlock()
+            .visitLabel(continueLabel)
+            .append(condition)
+            .ifZeroGoto(endLabel)
+            .append(body)
+            .gotoLabel(continueLabel)
+            .visitLabel(endLabel);
+
+        block.accept(visitor, generationContext);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(condition, body);
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitWhile(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/DebugNode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/DebugNode.java
new file mode 100644
index 0000000..80dffb3
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/DebugNode.java
@@ -0,0 +1,21 @@
+/*
+ * 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.facebook.presto.bytecode.debug;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+
+public interface DebugNode
+        extends BytecodeNode
+{
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LineNumberNode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LineNumberNode.java
new file mode 100644
index 0000000..f735cdb
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LineNumberNode.java
@@ -0,0 +1,59 @@
+/*
+ * 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.facebook.presto.bytecode.debug;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+public class LineNumberNode
+    implements DebugNode {
+    private final int lineNumber;
+    private final LabelNode label = new LabelNode();
+
+    public LineNumberNode(int lineNumber) {
+        this.lineNumber = lineNumber;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        if (generationContext.updateLineNumber(lineNumber)) {
+            label.accept(visitor, generationContext);
+            visitor.visitLineNumber(lineNumber, label.getLabel());
+        }
+    }
+
+    public int getLineNumber() {
+        return lineNumber;
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() +
+            "{line=" + lineNumber + '}';
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitLineNumber(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LocalVariableNode.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LocalVariableNode.java
new file mode 100644
index 0000000..c27cb73
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/debug/LocalVariableNode.java
@@ -0,0 +1,62 @@
+/*
+ * 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.facebook.presto.bytecode.debug;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.Variable;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+
+public class LocalVariableNode
+    implements DebugNode {
+    private final Variable variable;
+    private final LabelNode start;
+    private final LabelNode end;
+
+    public LocalVariableNode(Variable variable, LabelNode start, LabelNode end) {
+        this.variable = variable;
+        this.start = start;
+        this.end = end;
+    }
+
+    @Override
+    public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        visitor.visitLocalVariable(variable.getName(),
+            variable.getType().getType(),
+            variable.getType().getGenericSignature(),
+            start.getLabel(),
+            end.getLabel(),
+            generationContext.getVariableSlot(variable));
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "{variable=" + variable +
+            ", start=" + start +
+            ", end=" + end + '}';
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitLocalVariable(parent, this);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/AndBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/AndBytecodeExpression.java
new file mode 100644
index 0000000..ee1eaef
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/AndBytecodeExpression.java
@@ -0,0 +1,64 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class AndBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression left;
+    private final BytecodeExpression right;
+
+    AndBytecodeExpression(BytecodeExpression left, BytecodeExpression right) {
+        super(type(boolean.class));
+        this.left = requireNonNull(left, "left is null");
+        checkArgument(left.getType().getPrimitiveType() == boolean.class, "Expected left to be type boolean but is %s", left.getType());
+        this.right = requireNonNull(right, "right is null");
+        checkArgument(right.getType().getPrimitiveType() == boolean.class, "Expected right to be type boolean but is %s", right.getType());
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        LabelNode falseLabel = new LabelNode("false");
+        LabelNode endLabel = new LabelNode("end");
+        return new BytecodeBlock()
+            .append(left)
+            .ifFalseGoto(falseLabel)
+            .append(right)
+            .ifFalseGoto(falseLabel)
+            .push(true)
+            .gotoLabel(endLabel)
+            .visitLabel(falseLabel)
+            .push(false)
+            .visitLabel(endLabel);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(left, right);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return "(" + left + " && " + right + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArithmeticBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArithmeticBytecodeExpression.java
new file mode 100644
index 0000000..f3c31c5
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArithmeticBytecodeExpression.java
@@ -0,0 +1,202 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.OpCode;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+public class ArithmeticBytecodeExpression
+        extends BytecodeExpression
+{
+    public static BytecodeExpression createArithmeticBytecodeExpression(OpCode baseOpCode, BytecodeExpression left, BytecodeExpression right)
+    {
+        requireNonNull(baseOpCode, "baseOpCode is null");
+        String name = getName(baseOpCode);
+        String infixSymbol = getInfixSymbol(baseOpCode);
+
+        checkArgumentTypes(baseOpCode, name, left, right);
+
+        OpCode opCode = getNumericOpCode(name, baseOpCode, left.getType().getPrimitiveType());
+        return new ArithmeticBytecodeExpression(infixSymbol, left.getType(), opCode, left, right);
+    }
+
+    private static String getName(OpCode baseOpCode)
+    {
+        switch (baseOpCode) {
+            case IAND:
+                return "Bitwise AND";
+            case IOR:
+                return "Bitwise OR";
+            case IXOR:
+                return "Bitwise XOR";
+            case IADD:
+                return "Add";
+            case ISUB:
+                return "Subtract";
+            case IMUL:
+                return "Multiply";
+            case IDIV:
+                return "Divide";
+            case IREM:
+                return "Remainder";
+            case ISHL:
+                return "Shift left";
+            case ISHR:
+                return "Shift right";
+            case IUSHR:
+                return "Shift right unsigned";
+            default:
+                throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode);
+        }
+    }
+
+    private static String getInfixSymbol(OpCode baseOpCode)
+    {
+        switch (baseOpCode) {
+            case IAND:
+                return "&";
+            case IOR:
+                return "|";
+            case IXOR:
+                return "^";
+            case IADD:
+                return "+";
+            case ISUB:
+                return "-";
+            case IMUL:
+                return "*";
+            case IDIV:
+                return "/";
+            case IREM:
+                return "%";
+            case ISHL:
+                return "<<";
+            case ISHR:
+                return ">>";
+            case IUSHR:
+                return ">>>";
+            default:
+                throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode);
+        }
+    }
+
+    private static void checkArgumentTypes(OpCode baseOpCode, String name, BytecodeExpression left, BytecodeExpression right)
+    {
+        Class<?> leftType = getPrimitiveType(left, "left");
+        Class<?> rightType = getPrimitiveType(right, "right");
+        switch (baseOpCode) {
+            case IAND:
+            case IOR:
+            case IXOR:
+                checkArgument(leftType == rightType, "left and right must be the same type");
+                checkArgument(leftType == int.class || leftType == long.class, "%s argument must be int or long, but is %s", name, leftType);
+                return;
+            case IADD:
+            case ISUB:
+            case IMUL:
+            case IDIV:
+            case IREM:
+                checkArgument(leftType == rightType, "left and right must be the same type");
+                checkArgument(leftType == int.class || leftType == long.class || leftType == float.class || leftType == double.class,
+                        "%s argument must be int, long, float, or double, but is %s",
+                        name,
+                        leftType);
+                return;
+            case ISHL:
+            case ISHR:
+            case IUSHR:
+                checkArgument(leftType == int.class || leftType == long.class, "%s left argument be int or long, but is %s", name, leftType);
+                checkArgument(rightType == int.class, "%s right argument be and int, but is %s", name, rightType);
+                return;
+            default:
+                throw new IllegalArgumentException("Unsupported OpCode " + baseOpCode);
+        }
+    }
+
+    static OpCode getNumericOpCode(String name, OpCode baseOpCode, Class<?> type)
+    {
+        // Arithmetic OpCodes are laid out int, long, float and then double
+        if (type == int.class) {
+            return baseOpCode;
+        }
+        else if (type == long.class) {
+            return OpCode.getOpCode(baseOpCode.getOpCode() + 1);
+        }
+        else if (type == float.class) {
+            return OpCode.getOpCode(baseOpCode.getOpCode() + 2);
+        }
+        else if (type == double.class) {
+            return OpCode.getOpCode(baseOpCode.getOpCode() + 3);
+        }
+        else {
+            throw new IllegalArgumentException(name + " does not support " + type);
+        }
+    }
+
+    private static Class<?> getPrimitiveType(BytecodeExpression expression, String name)
+    {
+        requireNonNull(expression, name + " is null");
+        Class<?> leftType = expression.getType().getPrimitiveType();
+        checkArgument(leftType != null, name + " is not a primitive");
+        checkArgument(leftType != void.class, name + " is void");
+        return leftType;
+    }
+
+    private final String infixSymbol;
+    private final OpCode opCode;
+    private final BytecodeExpression left;
+    private final BytecodeExpression right;
+
+    private ArithmeticBytecodeExpression(
+            String infixSymbol,
+            ParameterizedType type,
+            OpCode opCode,
+            BytecodeExpression left,
+            BytecodeExpression right)
+    {
+        super(type);
+        this.infixSymbol = infixSymbol;
+        this.opCode = opCode;
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(left)
+                .append(right)
+                .append(opCode);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(left, right);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "(" + left + " " + infixSymbol + " " + right + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArrayLengthBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArrayLengthBytecodeExpression.java
new file mode 100644
index 0000000..ba56a83
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ArrayLengthBytecodeExpression.java
@@ -0,0 +1,55 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.OpCode.ARRAYLENGTH;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class ArrayLengthBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression instance;
+
+    ArrayLengthBytecodeExpression(BytecodeExpression instance)
+    {
+        super(type(int.class));
+        this.instance = requireNonNull(instance, "instance is null");
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(instance.getBytecode(generationContext))
+                .append(ARRAYLENGTH);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return instance + ".length";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpression.java
new file mode 100644
index 0000000..5ce36c1
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpression.java
@@ -0,0 +1,221 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.FieldDefinition;
+import com.facebook.presto.bytecode.MethodDefinition;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.objectweb.asm.MethodVisitor;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantInt;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+
+/**
+ * A BytecodeExpression is chain of Java like expressions that results in at most
+ * a single value being pushed on the stack.  The chain starts with a constant,
+ * local variable, static field, static method or invoke dynamic followed
+ * by zero or more invocations, field dereferences, array element fetches, or casts.
+ * The expression can optionally be terminated by a set expression, and in this
+ * case no value is pushed on the stack.
+ * <p>
+ * A BytecodeExpression is a BytecodeNode so it works with tools like tree dump.
+ * <p>
+ * This abstraction makes it easy to write generic byte code generators that can
+ * work with data that may come from a parameter, field or the result of a method
+ * invocation.
+ */
+public abstract class BytecodeExpression
+    implements BytecodeNode {
+    private final ParameterizedType type;
+
+    protected BytecodeExpression(ParameterizedType type) {
+        this.type = requireNonNull(type, "type is null");
+    }
+
+    public final ParameterizedType getType() {
+        return type;
+    }
+
+    public abstract BytecodeNode getBytecode(MethodGenerationContext generationContext);
+
+    protected abstract String formatOneLine();
+
+    @Override
+    public final String toString() {
+        return formatOneLine() + (type.getPrimitiveType() == void.class ? ";" : "");
+    }
+
+    public final BytecodeExpression getField(Class<?> declaringClass, String name) {
+        return new GetFieldBytecodeExpression(this, declaringClass, name);
+    }
+
+    public final BytecodeExpression getField(String name, Class<?> type) {
+        return new GetFieldBytecodeExpression(this, this.getType(), name, type(type));
+    }
+
+    public final BytecodeExpression getField(Field field) {
+        return new GetFieldBytecodeExpression(this, field);
+    }
+
+    public final BytecodeExpression getField(FieldDefinition field) {
+        return new GetFieldBytecodeExpression(this, field);
+    }
+
+    public final BytecodeExpression getField(ParameterizedType declaringClass, String name, ParameterizedType type) {
+        return new GetFieldBytecodeExpression(this, declaringClass, name, type);
+    }
+
+    public final BytecodeExpression setField(String name, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(this, this.getType(), name, value);
+    }
+
+    public final BytecodeExpression setField(Field field, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(this, field, value);
+    }
+
+    public final BytecodeExpression setField(FieldDefinition field, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(this, field, value);
+    }
+
+    public final BytecodeExpression cast(Class<?> type) {
+        return new CastBytecodeExpression(this, type(type));
+    }
+
+    public final BytecodeExpression cast(ParameterizedType type) {
+        return new CastBytecodeExpression(this, type);
+    }
+
+    public final BytecodeExpression invoke(Method method, BytecodeExpression... parameters) {
+        return invoke(method, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public final BytecodeExpression invoke(MethodDefinition method,
+        Collection<? extends BytecodeExpression> parameters) {
+        List<BytecodeExpression> params = new ArrayList<>(parameters);
+
+        checkArgument(method.getParameters().size() == params.size(), "Expected %s params found %s", method.getParameters().size(), params.size());
+        return invoke(method.getName(), method.getReturnType(), method.getParameterTypes(), parameters);
+    }
+
+    public final BytecodeExpression invoke(Method method, Collection<? extends BytecodeExpression> parameters) {
+        return invoke(
+            method.getName(),
+            type(method.getReturnType()),
+            stream(method.getParameterTypes())
+                .map(ParameterizedType::type)
+                .collect(Collectors.toList()),
+            parameters);
+    }
+
+    public final BytecodeExpression invoke(String methodName, Class<?> returnType, BytecodeExpression... parameters) {
+        return invoke(methodName, type(returnType), List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public final BytecodeExpression invoke(String methodName, Class<?> returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        return invoke(methodName, type(returnType), parameters);
+    }
+
+    public final BytecodeExpression invoke(String methodName, ParameterizedType returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        requireNonNull(parameters, "parameters is null");
+
+        return invoke(methodName,
+            returnType,
+            parameters.stream().map(BytecodeExpression::getType).collect(Collectors.toList()),
+            parameters);
+    }
+
+    public final BytecodeExpression invoke(String methodName, Class<?> returnType,
+        Collection<? extends Class<?>> parameterTypes, BytecodeExpression... parameters) {
+        return invoke(methodName, type(returnType),
+            parameterTypes.stream().map(ParameterizedType::type).collect(Collectors.toList()),
+            List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public final BytecodeExpression invoke(String methodName, ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes, BytecodeExpression... parameters) {
+        return invoke(methodName, returnType, parameterTypes, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public final BytecodeExpression invoke(
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        return InvokeBytecodeExpression.createInvoke(
+            this,
+            methodName,
+            returnType,
+            parameterTypes,
+            parameters);
+    }
+
+    public final BytecodeExpression getElement(int index) {
+        return new GetElementBytecodeExpression(this, constantInt(index));
+    }
+
+    public final BytecodeExpression getElement(BytecodeExpression index) {
+        return new GetElementBytecodeExpression(this, index);
+    }
+
+    public final BytecodeExpression setElement(int index, BytecodeExpression value) {
+        return new SetArrayElementBytecodeExpression(this, constantInt(index), value);
+    }
+
+    public final BytecodeExpression setElement(BytecodeExpression index, BytecodeExpression value) {
+        return new SetArrayElementBytecodeExpression(this, index, value);
+    }
+
+    public final BytecodeExpression length() {
+        return new ArrayLengthBytecodeExpression(this);
+    }
+
+    public final BytecodeExpression ret() {
+        return new ReturnBytecodeExpression(this);
+    }
+
+    public final BytecodeExpression pop() {
+        if (this.getType().getPrimitiveType() == void.class) {
+            return this;
+        }
+        return new PopBytecodeExpression(this);
+    }
+
+    @Override
+    public final void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+        getBytecode(generationContext).accept(visitor, generationContext);
+    }
+
+    @Override
+    public final <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitBytecodeExpression(parent, this);
+    }
+
+    public BytecodeExpression instanceOf(Class<?> type) {
+        return InstanceOfBytecodeExpression.instanceOf(this, type);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpressions.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpressions.java
new file mode 100644
index 0000000..448ee8a
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/BytecodeExpressions.java
@@ -0,0 +1,623 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.FieldDefinition;
+import com.facebook.presto.bytecode.MethodDefinition;
+import com.facebook.presto.bytecode.OpCode;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.expression.ArithmeticBytecodeExpression.createArithmeticBytecodeExpression;
+import static com.facebook.presto.bytecode.instruction.Constant.loadBoolean;
+import static com.facebook.presto.bytecode.instruction.Constant.loadClass;
+import static com.facebook.presto.bytecode.instruction.Constant.loadDouble;
+import static com.facebook.presto.bytecode.instruction.Constant.loadFloat;
+import static com.facebook.presto.bytecode.instruction.Constant.loadInt;
+import static com.facebook.presto.bytecode.instruction.Constant.loadLong;
+import static com.facebook.presto.bytecode.instruction.Constant.loadNull;
+import static com.facebook.presto.bytecode.instruction.Constant.loadString;
+import static java.util.Arrays.stream;
+import static java.util.Objects.requireNonNull;
+
+public final class BytecodeExpressions {
+    private BytecodeExpressions() {
+    }
+
+    //
+    // Constants
+    //
+
+    public static BytecodeExpression constantTrue() {
+        return new ConstantBytecodeExpression(boolean.class, loadBoolean(true));
+    }
+
+    public static BytecodeExpression constantFalse() {
+        return new ConstantBytecodeExpression(boolean.class, loadBoolean(false));
+    }
+
+    public static BytecodeExpression constantBoolean(boolean value) {
+        return new ConstantBytecodeExpression(boolean.class, loadBoolean(value));
+    }
+
+    public static BytecodeExpression constantClass(Class<?> value) {
+        return new ConstantBytecodeExpression(Class.class, loadClass(value));
+    }
+
+    public static BytecodeExpression constantClass(ParameterizedType value) {
+        return new ConstantBytecodeExpression(Class.class, loadClass(value));
+    }
+
+    public static BytecodeExpression constantDouble(double value) {
+        return new ConstantBytecodeExpression(double.class, loadDouble(value));
+    }
+
+    public static BytecodeExpression constantFloat(float value) {
+        return new ConstantBytecodeExpression(float.class, loadFloat(value));
+    }
+
+    public static BytecodeExpression constantInt(int value) {
+        return new ConstantBytecodeExpression(int.class, loadInt(value));
+    }
+
+    public static BytecodeExpression constantLong(long value) {
+        return new ConstantBytecodeExpression(long.class, loadLong(value));
+    }
+
+    public static BytecodeExpression constantNumber(Number value) {
+        if (value instanceof Byte) {
+            return constantInt((value).intValue()).cast(byte.class);
+        }
+        if (value instanceof Short) {
+            return constantInt((value).intValue()).cast(short.class);
+        }
+        if (value instanceof Integer) {
+            return constantInt((Integer)value);
+        }
+        if (value instanceof Long) {
+            return constantLong((Long)value);
+        }
+        if (value instanceof Float) {
+            return constantFloat((Float)value);
+        }
+        if (value instanceof Double) {
+            return constantDouble((Double)value);
+        }
+        throw new IllegalStateException("Unsupported number type " + value.getClass().getSimpleName());
+    }
+
+    public static BytecodeExpression constantNull(Class<?> type) {
+        return new ConstantBytecodeExpression(type, loadNull());
+    }
+
+    public static BytecodeExpression constantNull(ParameterizedType type) {
+        return new ConstantBytecodeExpression(type, loadNull());
+    }
+
+    public static BytecodeExpression constantString(String value) {
+        return new ConstantBytecodeExpression(String.class, loadString(value));
+    }
+
+    public static BytecodeExpression defaultValue(ParameterizedType type) {
+        if (type.isPrimitive()) {
+            return defaultValue(type.getPrimitiveType());
+        }
+        return constantNull(type);
+    }
+
+    public static BytecodeExpression defaultValue(Class<?> type) {
+        requireNonNull(type, "type is null");
+        if (type == boolean.class) {
+            return constantInt(0).cast(boolean.class);
+        }
+        if (type == byte.class) {
+            return constantInt(0).cast(byte.class);
+        }
+        if (type == int.class) {
+            return constantInt(0);
+        }
+        if (type == short.class) {
+            return constantInt(0).cast(short.class);
+        }
+        if (type == long.class) {
+            return constantLong(0L);
+        }
+        if (type == float.class) {
+            return constantFloat(0.0f);
+        }
+        if (type == double.class) {
+            return constantDouble(0.0d);
+        }
+        checkArgument(!type.isPrimitive(), "Unsupported type %s", type);
+        return constantNull(type);
+    }
+
+    //
+    // Get static field
+    //
+
+    public static BytecodeExpression getStatic(Class<?> declaringClass, String name) {
+        return new GetFieldBytecodeExpression(null, declaringClass, name);
+    }
+
+    public static BytecodeExpression getStatic(Field staticField) {
+        return new GetFieldBytecodeExpression(null, staticField);
+    }
+
+    public static BytecodeExpression getStatic(FieldDefinition staticField) {
+        return new GetFieldBytecodeExpression(null, staticField);
+    }
+
+    public static BytecodeExpression getStatic(ParameterizedType declaringClass, String name, ParameterizedType type) {
+        return new GetFieldBytecodeExpression(null, declaringClass, name, type);
+    }
+
+    //
+    // Set static field
+    //
+
+    public static BytecodeExpression setStatic(Class<?> declaringClass, String name, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(null, declaringClass, name, value);
+    }
+
+    public static BytecodeExpression setStatic(Field staticField, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(null, staticField, value);
+    }
+
+    public static BytecodeExpression setStatic(FieldDefinition staticField, BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(null, staticField, value);
+    }
+
+    public static BytecodeExpression setStatic(ParameterizedType declaringClass, String name,
+        BytecodeExpression value) {
+        return new SetFieldBytecodeExpression(null, declaringClass, name, value);
+    }
+
+    //
+    // New instance
+    //
+
+    public static BytecodeExpression newInstance(Constructor<?> constructor, BytecodeExpression... parameters) {
+        return newInstance(constructor, List.of(parameters));
+    }
+
+    public static BytecodeExpression newInstance(Constructor<?> constructor,
+        Collection<? extends BytecodeExpression> parameters) {
+        return newInstance(
+            type(constructor.getDeclaringClass()),
+            stream(constructor.getParameterTypes())
+                .map(ParameterizedType::type)
+                .collect(Collectors.toUnmodifiableList()),
+            parameters);
+    }
+
+    public static BytecodeExpression newInstance(Class<?> returnType, BytecodeExpression... parameters) {
+        return newInstance(type(returnType), List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression newInstance(Class<?> returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        return newInstance(type(returnType), parameters);
+    }
+
+    public static BytecodeExpression newInstance(ParameterizedType returnType, BytecodeExpression... parameters) {
+        requireNonNull(parameters, "parameters is null");
+
+        return newInstance(returnType, List.of(parameters));
+    }
+
+    public static BytecodeExpression newInstance(ParameterizedType returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        requireNonNull(parameters, "parameters is null");
+
+        return newInstance(
+            returnType,
+            parameters.stream().map(BytecodeExpression::getType).collect(Collectors.toList()),
+            parameters);
+    }
+
+    public static BytecodeExpression newInstance(Class<?> returnType, Collection<? extends Class<?>> parameterTypes,
+        BytecodeExpression... parameters) {
+        return newInstance(type(returnType), parameterTypes.stream().map(ParameterizedType::type).collect(Collectors.toList()),
+            List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression newInstance(ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes, BytecodeExpression... parameters) {
+        return newInstance(returnType, parameterTypes, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression newInstance(
+        ParameterizedType type,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        return new NewInstanceBytecodeExpression(type, parameterTypes, parameters);
+    }
+
+    //
+    // Array
+    //
+    public static BytecodeExpression newArray(ParameterizedType type, int length) {
+        return new NewArrayBytecodeExpression(type, length);
+    }
+
+    public static BytecodeExpression newArray(ParameterizedType type, BytecodeExpression length) {
+        return new NewArrayBytecodeExpression(type, length);
+    }
+
+    public static BytecodeExpression newArray(ParameterizedType type, BytecodeExpression... elements) {
+        return new NewArrayBytecodeExpression(type, List.of(elements));
+    }
+
+    public static BytecodeExpression newArray(ParameterizedType type,
+        Collection<? extends BytecodeExpression> elements) {
+        return new NewArrayBytecodeExpression(type, List.copyOf(elements));
+    }
+
+    public static BytecodeExpression length(BytecodeExpression instance) {
+        return new ArrayLengthBytecodeExpression(instance);
+    }
+
+    public static BytecodeExpression get(BytecodeExpression instance, BytecodeExpression index) {
+        return new GetElementBytecodeExpression(instance, index);
+    }
+
+    public static BytecodeExpression set(BytecodeExpression instance, BytecodeExpression index,
+        BytecodeExpression value) {
+        return new SetArrayElementBytecodeExpression(instance, index, value);
+    }
+
+    //
+    // Invoke static method
+    //
+
+    public static BytecodeExpression invokeStatic(MethodDefinition method, BytecodeExpression... parameters) {
+        return invokeStatic(
+            method.getDeclaringClass().getType(),
+            method.getName(),
+            method.getReturnType(),
+            method.getParameterTypes(),
+            List.of(parameters));
+    }
+
+    public static BytecodeExpression invokeStatic(Method method, BytecodeExpression... parameters) {
+        return invokeStatic(method, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeStatic(Method method, Collection<? extends BytecodeExpression> parameters) {
+        return invokeStatic(
+            type(method.getDeclaringClass()),
+            method.getName(),
+            type(method.getReturnType()),
+            stream(method.getParameterTypes())
+                .map(ParameterizedType::type)
+                .collect(Collectors.toUnmodifiableList()),
+            parameters);
+    }
+
+    public static BytecodeExpression invokeStatic(Class<?> methodTargetType, String methodName, Class<?> returnType,
+        BytecodeExpression... parameters) {
+        return invokeStatic(methodTargetType, methodName, returnType, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeStatic(
+        Class<?> methodTargetType,
+        String methodName,
+        Class<?> returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        return invokeStatic(type(methodTargetType), methodName, type(returnType), parameters);
+    }
+
+    public static BytecodeExpression invokeStatic(
+        ParameterizedType methodTargetType,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        requireNonNull(methodTargetType, "methodTargetType is null");
+        requireNonNull(returnType, "returnType is null");
+        requireNonNull(parameters, "parameters is null");
+
+        return invokeStatic(
+            methodTargetType,
+            methodName,
+            returnType,
+            parameters.stream()
+                .map(BytecodeExpression::getType)
+                .collect(Collectors.toUnmodifiableList()),
+            parameters);
+    }
+
+    public static BytecodeExpression invokeStatic(
+        Class<?> methodTargetType,
+        String methodName,
+        Class<?> returnType,
+        Collection<? extends Class<?>> parameterTypes,
+        BytecodeExpression... parameters) {
+        requireNonNull(methodTargetType, "methodTargetType is null");
+        requireNonNull(returnType, "returnType is null");
+        requireNonNull(parameterTypes, "parameterTypes is null");
+        requireNonNull(parameters, "parameters is null");
+
+        return invokeStatic(
+            type(methodTargetType),
+            methodName,
+            type(returnType),
+            parameterTypes.stream()
+                .map(ParameterizedType::type)
+                .collect(Collectors.toUnmodifiableList()),
+            List.of(parameters));
+    }
+
+    public static BytecodeExpression invokeStatic(
+        ParameterizedType methodTargetType,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        BytecodeExpression... parameters) {
+        return invokeStatic(methodTargetType, methodName, returnType, parameterTypes, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeStatic(
+        ParameterizedType methodTargetType,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        return new InvokeBytecodeExpression(
+            null,
+            methodTargetType,
+            methodName,
+            returnType,
+            parameterTypes,
+            parameters);
+    }
+
+    //
+    // Invoke dynamic
+    //
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        Class<?> returnType,
+        BytecodeExpression... parameters) {
+        return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, returnType, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        Class<?> returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        requireNonNull(returnType, "returnType is null");
+        requireNonNull(parameters, "parameters is null");
+
+        return invokeDynamic(
+            bootstrapMethod,
+            bootstrapArgs,
+            methodName,
+            type(returnType),
+            parameters.stream()
+                .map(BytecodeExpression::getType)
+                .collect(Collectors.toUnmodifiableList()),
+            parameters);
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        ParameterizedType returnType,
+        BytecodeExpression... parameters) {
+        return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, returnType, List.of(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<? extends BytecodeExpression> parameters) {
+        requireNonNull(returnType, "returnType is null");
+        requireNonNull(parameters, "parameters is null");
+
+        return invokeDynamic(
+            bootstrapMethod,
+            bootstrapArgs,
+            methodName,
+            returnType,
+            parameters.stream()
+                .map(BytecodeExpression::getType)
+                .collect(Collectors.toUnmodifiableList()),
+            parameters);
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        MethodType methodType,
+        BytecodeExpression... parameters) {
+        requireNonNull(methodType, "methodType is null");
+        requireNonNull(parameters, "parameters is null");
+
+        return invokeDynamic(bootstrapMethod, bootstrapArgs, methodName, methodType, List.of(parameters));
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        MethodType methodType,
+        Collection<? extends BytecodeExpression> parameters) {
+        return invokeDynamic(
+            bootstrapMethod,
+            bootstrapArgs,
+            methodName,
+            type(methodType.returnType()),
+            methodType.parameterList().stream().map(ParameterizedType::type).collect(Collectors.toList()),
+            List.copyOf(requireNonNull(parameters, "parameters is null")));
+    }
+
+    public static BytecodeExpression invokeDynamic(
+        Method bootstrapMethod,
+        Collection<? extends Object> bootstrapArgs,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        return new InvokeDynamicBytecodeExpression(
+            bootstrapMethod,
+            bootstrapArgs,
+            methodName,
+            returnType,
+            parameters,
+            parameterTypes);
+    }
+
+    //
+    // Arithmetic operations
+    //
+
+    public static BytecodeExpression add(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IADD, left, right);
+    }
+
+    public static BytecodeExpression subtract(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.ISUB, left, right);
+    }
+
+    public static BytecodeExpression multiply(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IMUL, left, right);
+    }
+
+    public static BytecodeExpression divide(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IDIV, left, right);
+    }
+
+    public static BytecodeExpression remainder(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IREM, left, right);
+    }
+
+    public static BytecodeExpression bitwiseAnd(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IAND, left, right);
+    }
+
+    public static BytecodeExpression bitwiseOr(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IOR, left, right);
+    }
+
+    public static BytecodeExpression bitwiseXor(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IXOR, left, right);
+    }
+
+    public static BytecodeExpression shiftLeft(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.ISHL, left, right);
+    }
+
+    public static BytecodeExpression shiftRight(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.ISHR, left, right);
+    }
+
+    public static BytecodeExpression shiftRightUnsigned(BytecodeExpression left, BytecodeExpression right) {
+        return createArithmeticBytecodeExpression(OpCode.IUSHR, left, right);
+    }
+
+    public static BytecodeExpression negate(BytecodeExpression value) {
+        return new NegateBytecodeExpression(value);
+    }
+
+    //
+    // Comparison operations
+    //
+
+    public static BytecodeExpression lessThan(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.lessThan(left, right);
+    }
+
+    public static BytecodeExpression greaterThan(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.greaterThan(left, right);
+    }
+
+    public static BytecodeExpression lessThanOrEqual(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.lessThanOrEqual(left, right);
+    }
+
+    public static BytecodeExpression greaterThanOrEqual(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.greaterThanOrEqual(left, right);
+    }
+
+    public static BytecodeExpression equal(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.equal(left, right);
+    }
+
+    public static BytecodeExpression notEqual(BytecodeExpression left, BytecodeExpression right) {
+        return ComparisonBytecodeExpression.notEqual(left, right);
+    }
+
+    //
+    // Null comparison operations
+    //
+
+    public static BytecodeExpression isNull(BytecodeExpression value) {
+        return equal(value, constantNull(value.getType()));
+    }
+
+    public static BytecodeExpression isNotNull(BytecodeExpression value) {
+        return notEqual(value, constantNull(value.getType()));
+    }
+
+    //
+    // Logical binary operations
+    //
+
+    public static BytecodeExpression and(BytecodeExpression left, BytecodeExpression right) {
+        return new AndBytecodeExpression(left, right);
+    }
+
+    public static BytecodeExpression or(BytecodeExpression left, BytecodeExpression right) {
+        return new OrBytecodeExpression(left, right);
+    }
+
+    public static BytecodeExpression not(BytecodeExpression value) {
+        return new NotBytecodeExpression(value);
+    }
+
+    //
+    // Complex expressions
+    //
+
+    public static BytecodeExpression inlineIf(BytecodeExpression condition, BytecodeExpression ifTrue,
+        BytecodeExpression ifFalse) {
+        return new InlineIfBytecodeExpression(condition, ifTrue, ifFalse);
+    }
+
+    //
+    // Print
+    //
+    public static BytecodeExpression print(BytecodeExpression variable) {
+        BytecodeExpression out = getStatic(System.class, "out");
+        return out.invoke("println", void.class, variable);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/CastBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/CastBytecodeExpression.java
new file mode 100644
index 0000000..d08cc83
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/CastBytecodeExpression.java
@@ -0,0 +1,328 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeUtils;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.OpCode;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+class CastBytecodeExpression
+        extends BytecodeExpression
+{
+    private static final ParameterizedType OBJECT_TYPE = type(Object.class);
+
+    private final BytecodeExpression instance;
+
+    CastBytecodeExpression(BytecodeExpression instance, ParameterizedType type)
+    {
+        super(type);
+
+        this.instance = requireNonNull(instance, "instance is null");
+
+        checkArgument(type.getPrimitiveType() != void.class, "Type %s can not be cast to %s", instance.getType(), type);
+
+        // Call generateBytecode to run the validation logic. The result is thrown away.
+        // Duplicating the validation logic here is error-prone and introduces duplicate code.
+        generateBytecode(instance.getType(), getType());
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(instance.getBytecode(generationContext))
+                .append(generateBytecode(instance.getType(), getType()));
+    }
+
+    private static BytecodeBlock generateBytecode(ParameterizedType sourceType, ParameterizedType targetType)
+    {
+        BytecodeBlock block = new BytecodeBlock();
+
+        switch (getTypeKind(sourceType)) {
+            case PRIMITIVE:
+                switch (getTypeKind(targetType)) {
+                    case PRIMITIVE:
+                        castPrimitiveToPrimitive(block, sourceType.getPrimitiveType(), targetType.getPrimitiveType());
+                        return block;
+                    case BOXED_PRIMITVE:
+                        checkArgument(sourceType.getPrimitiveType() == unwrapPrimitiveType(targetType), "Type %s can not be cast to %s", sourceType, targetType);
+                        return block.invokeStatic(targetType, "valueOf", targetType, sourceType);
+                    case OTHER:
+                        checkArgument(OBJECT_TYPE.equals(targetType), "Type %s can not be cast to %s", sourceType, targetType);
+                        Class<?> sourceClass = sourceType.getPrimitiveType();
+                        return block
+                                .invokeStatic(BytecodeUtils.wrap(sourceClass), "valueOf", BytecodeUtils.wrap(sourceClass), sourceClass)
+                                .checkCast(targetType);
+                }
+            case BOXED_PRIMITVE:
+                switch (getTypeKind(targetType)) {
+                    case PRIMITIVE:
+                        checkArgument(unwrapPrimitiveType(sourceType) == targetType.getPrimitiveType(), "Type %s can not be cast to %s", sourceType, targetType);
+                        return block.invokeVirtual(sourceType, targetType.getPrimitiveType().getSimpleName() + "Value", targetType);
+                    case BOXED_PRIMITVE:
+                        checkArgument(sourceType.equals(targetType), "Type %s can not be cast to %s", sourceType, targetType);
+                        return block;
+                    case OTHER:
+                        return block.checkCast(targetType);
+                }
+            case OTHER:
+                switch (getTypeKind(targetType)) {
+                    case PRIMITIVE:
+                        checkArgument(OBJECT_TYPE.equals(sourceType), "Type %s can not be cast to %s", sourceType, targetType);
+                        return block
+                                .checkCast(BytecodeUtils.wrap(targetType.getPrimitiveType()))
+                                .invokeVirtual(BytecodeUtils.wrap(targetType.getPrimitiveType()), targetType.getPrimitiveType().getSimpleName() + "Value", targetType.getPrimitiveType());
+                    case BOXED_PRIMITVE:
+                    case OTHER:
+                        return block.checkCast(targetType);
+                }
+        }
+        throw new UnsupportedOperationException("unexpected enum value");
+    }
+
+    private static BytecodeBlock castPrimitiveToPrimitive(BytecodeBlock block, Class<?> sourceType, Class<?> targetType)
+    {
+        if (sourceType == boolean.class) {
+            if (targetType == boolean.class) {
+                return block;
+            }
+        }
+        if (sourceType == byte.class) {
+            if (targetType == byte.class) {
+                return block;
+            }
+            if (targetType == char.class) {
+                return block;
+            }
+            if (targetType == short.class) {
+                return block;
+            }
+            if (targetType == int.class) {
+                return block;
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.I2L);
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.I2F);
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.I2D);
+            }
+        }
+        if (sourceType == char.class) {
+            if (targetType == byte.class) {
+                return block.append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block;
+            }
+            if (targetType == short.class) {
+                return block;
+            }
+            if (targetType == int.class) {
+                return block;
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.I2L);
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.I2F);
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.I2D);
+            }
+        }
+        if (sourceType == short.class) {
+            if (targetType == byte.class) {
+                return block.append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block.append(OpCode.I2C);
+            }
+            if (targetType == short.class) {
+                return block;
+            }
+            if (targetType == int.class) {
+                return block;
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.I2L);
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.I2F);
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.I2D);
+            }
+        }
+        if (sourceType == int.class) {
+            if (targetType == boolean.class) {
+                return block;
+            }
+            if (targetType == byte.class) {
+                return block.append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block.append(OpCode.I2C);
+            }
+            if (targetType == short.class) {
+                return block.append(OpCode.I2S);
+            }
+            if (targetType == int.class) {
+                return block;
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.I2L);
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.I2F);
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.I2D);
+            }
+        }
+        if (sourceType == long.class) {
+            if (targetType == byte.class) {
+                return block.append(OpCode.L2I).append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block.append(OpCode.L2I).append(OpCode.I2C);
+            }
+            if (targetType == short.class) {
+                return block.append(OpCode.L2I).append(OpCode.I2S);
+            }
+            if (targetType == int.class) {
+                return block.append(OpCode.L2I);
+            }
+            if (targetType == long.class) {
+                return block;
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.L2F);
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.L2D);
+            }
+        }
+        if (sourceType == float.class) {
+            if (targetType == byte.class) {
+                return block.append(OpCode.F2I).append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block.append(OpCode.F2I).append(OpCode.I2C);
+            }
+            if (targetType == short.class) {
+                return block.append(OpCode.F2I).append(OpCode.I2S);
+            }
+            if (targetType == int.class) {
+                return block.append(OpCode.F2I);
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.F2L);
+            }
+            if (targetType == float.class) {
+                return block;
+            }
+            if (targetType == double.class) {
+                return block.append(OpCode.F2D);
+            }
+        }
+        if (sourceType == double.class) {
+            if (targetType == byte.class) {
+                return block.append(OpCode.D2I).append(OpCode.I2B);
+            }
+            if (targetType == char.class) {
+                return block.append(OpCode.D2I).append(OpCode.I2C);
+            }
+            if (targetType == short.class) {
+                return block.append(OpCode.D2I).append(OpCode.I2S);
+            }
+            if (targetType == int.class) {
+                return block.append(OpCode.D2I);
+            }
+            if (targetType == long.class) {
+                return block.append(OpCode.D2L);
+            }
+            if (targetType == float.class) {
+                return block.append(OpCode.D2F);
+            }
+            if (targetType == double.class) {
+                return block;
+            }
+        }
+        throw new IllegalArgumentException(format("Type %s can not be cast to %s", sourceType, targetType));
+    }
+
+    private static TypeKind getTypeKind(ParameterizedType type)
+    {
+        if (type.isPrimitive()) {
+            return TypeKind.PRIMITIVE;
+        }
+        if (unwrapPrimitiveType(type) != null) {
+            return TypeKind.BOXED_PRIMITVE;
+        }
+        return TypeKind.OTHER;
+    }
+
+    private static Class<?> unwrapPrimitiveType(ParameterizedType boxedPrimitiveType)
+    {
+        switch (boxedPrimitiveType.getJavaClassName()) {
+            case "java.lang.Boolean":
+                return boolean.class;
+            case "java.lang.Byte":
+                return byte.class;
+            case "java.lang.Character":
+                return char.class;
+            case "java.lang.Short":
+                return short.class;
+            case "java.lang.Integer":
+                return int.class;
+            case "java.lang.Long":
+                return long.class;
+            case "java.lang.Float":
+                return float.class;
+            case "java.lang.Double":
+                return double.class;
+            default:
+                return null;
+        }
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "((" + getType().getSimpleName() + ") " + instance + ")";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(instance);
+    }
+
+    private enum TypeKind
+    {
+        PRIMITIVE, BOXED_PRIMITVE, OTHER
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ComparisonBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ComparisonBytecodeExpression.java
new file mode 100644
index 0000000..8267648
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ComparisonBytecodeExpression.java
@@ -0,0 +1,313 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.OpCode;
+import com.facebook.presto.bytecode.instruction.JumpInstruction;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.OpCode.DCMPG;
+import static com.facebook.presto.bytecode.OpCode.DCMPL;
+import static com.facebook.presto.bytecode.OpCode.FCMPG;
+import static com.facebook.presto.bytecode.OpCode.FCMPL;
+import static com.facebook.presto.bytecode.OpCode.IFEQ;
+import static com.facebook.presto.bytecode.OpCode.IFGE;
+import static com.facebook.presto.bytecode.OpCode.IFGT;
+import static com.facebook.presto.bytecode.OpCode.IFLE;
+import static com.facebook.presto.bytecode.OpCode.IFLT;
+import static com.facebook.presto.bytecode.OpCode.IFNE;
+import static com.facebook.presto.bytecode.OpCode.IF_ACMPEQ;
+import static com.facebook.presto.bytecode.OpCode.IF_ACMPNE;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPEQ;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPGE;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPGT;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPLE;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPLT;
+import static com.facebook.presto.bytecode.OpCode.IF_ICMPNE;
+import static com.facebook.presto.bytecode.OpCode.LCMP;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class ComparisonBytecodeExpression
+        extends BytecodeExpression
+{
+    static BytecodeExpression lessThan(BytecodeExpression left, BytecodeExpression right)
+    {
+        checkArgumentTypes(left, right);
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPGE;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFGE;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPG;
+            noMatchJumpInstruction = IFGE;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPG;
+            noMatchJumpInstruction = IFGE;
+        }
+        else {
+            throw new IllegalArgumentException("Less than does not support " + type);
+        }
+
+        return new ComparisonBytecodeExpression("<", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    static BytecodeExpression greaterThan(BytecodeExpression left, BytecodeExpression right)
+    {
+        checkArgumentTypes(left, right);
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPLE;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFLE;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPL;
+            noMatchJumpInstruction = IFLE;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPL;
+            noMatchJumpInstruction = IFLE;
+        }
+        else {
+            throw new IllegalArgumentException("Greater than does not support " + type);
+        }
+        return new ComparisonBytecodeExpression(">", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    static BytecodeExpression lessThanOrEqual(BytecodeExpression left, BytecodeExpression right)
+    {
+        checkArgumentTypes(left, right);
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPGT;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFGT;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPG;
+            noMatchJumpInstruction = IFGT;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPG;
+            noMatchJumpInstruction = IFGT;
+        }
+        else {
+            throw new IllegalArgumentException("Less than or equal does not support " + type);
+        }
+        return new ComparisonBytecodeExpression("<=", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    static BytecodeExpression greaterThanOrEqual(BytecodeExpression left, BytecodeExpression right)
+    {
+        checkArgumentTypes(left, right);
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPLT;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFLT;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPL;
+            noMatchJumpInstruction = IFLT;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPL;
+            noMatchJumpInstruction = IFLT;
+        }
+        else {
+            throw new IllegalArgumentException("Greater than or equal does not support " + type);
+        }
+        return new ComparisonBytecodeExpression(">=", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    static BytecodeExpression equal(BytecodeExpression left, BytecodeExpression right)
+    {
+        requireNonNull(left, "left is null");
+        requireNonNull(right, "right is null");
+        checkArgument(left.getType().equals(right.getType()), "left and right must be the same type");
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPNE;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFNE;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPL;
+            noMatchJumpInstruction = IFNE;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPL;
+            noMatchJumpInstruction = IFNE;
+        }
+        else if (type == null) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ACMPNE;
+        }
+        else {
+            throw new IllegalArgumentException("Equal does not support " + type);
+        }
+        return new ComparisonBytecodeExpression("==", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    static BytecodeExpression notEqual(BytecodeExpression left, BytecodeExpression right)
+    {
+        requireNonNull(left, "left is null");
+        requireNonNull(right, "right is null");
+        checkArgument(left.getType().equals(right.getType()), "left and right must be the same type");
+
+        OpCode comparisonInstruction;
+        OpCode noMatchJumpInstruction;
+
+        Class<?> type = left.getType().getPrimitiveType();
+        if (type == int.class) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ICMPEQ;
+        }
+        else if (type == long.class) {
+            comparisonInstruction = LCMP;
+            noMatchJumpInstruction = IFEQ;
+        }
+        else if (type == float.class) {
+            comparisonInstruction = FCMPL;
+            noMatchJumpInstruction = IFEQ;
+        }
+        else if (type == double.class) {
+            comparisonInstruction = DCMPL;
+            noMatchJumpInstruction = IFEQ;
+        }
+        else if (type == null) {
+            comparisonInstruction = null;
+            noMatchJumpInstruction = IF_ACMPEQ;
+        }
+        else {
+            throw new IllegalArgumentException("Not equal than does not support " + type);
+        }
+        return new ComparisonBytecodeExpression("!=", comparisonInstruction, noMatchJumpInstruction, left, right);
+    }
+
+    private static void checkArgumentTypes(BytecodeExpression left, BytecodeExpression right)
+    {
+        Class<?> leftType = getPrimitiveType(left, "left");
+        Class<?> rightType = getPrimitiveType(right, "right");
+        checkArgument(leftType == rightType, "left and right must be the same type");
+    }
+
+    private static Class<?> getPrimitiveType(BytecodeExpression expression, String name)
+    {
+        requireNonNull(expression, name + " is null");
+        Class<?> leftType = expression.getType().getPrimitiveType();
+        checkArgument(leftType != null, name + " is not a primitive");
+        checkArgument(leftType != void.class, name + " is void");
+        return leftType;
+    }
+
+    private final String infixSymbol;
+    private final OpCode comparisonInstruction;
+    private final OpCode noMatchJumpInstruction;
+    private final BytecodeExpression left;
+    private final BytecodeExpression right;
+
+    private ComparisonBytecodeExpression(
+            String infixSymbol,
+            OpCode comparisonInstruction,
+            OpCode noMatchJumpInstruction,
+            BytecodeExpression left,
+            BytecodeExpression right)
+    {
+        super(type(boolean.class));
+        this.infixSymbol = infixSymbol;
+        this.comparisonInstruction = comparisonInstruction;
+        this.noMatchJumpInstruction = noMatchJumpInstruction;
+        this.left = left;
+        this.right = right;
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        BytecodeBlock block = new BytecodeBlock()
+                .append(left)
+                .append(right);
+
+        if (comparisonInstruction != null) {
+            block.append(comparisonInstruction);
+        }
+
+        LabelNode noMatch = new LabelNode("no_match");
+        LabelNode end = new LabelNode("end");
+        return block
+                .append(new JumpInstruction(noMatchJumpInstruction, noMatch))
+                .push(true)
+                .gotoLabel(end)
+                .append(noMatch)
+                .push(false)
+                .append(end);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(left, right);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "(" + left + " " + infixSymbol + " " + right + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ConstantBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ConstantBytecodeExpression.java
new file mode 100644
index 0000000..e1fc0f2
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ConstantBytecodeExpression.java
@@ -0,0 +1,68 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import com.facebook.presto.bytecode.instruction.Constant;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+
+class ConstantBytecodeExpression
+    extends BytecodeExpression {
+    private final Constant value;
+
+    ConstantBytecodeExpression(Class<?> type, Constant value) {
+        this(type(type), value);
+    }
+
+    ConstantBytecodeExpression(ParameterizedType type, Constant value) {
+        super(type);
+        this.value = value;
+    }
+
+    @Override
+    public Constant getBytecode(MethodGenerationContext generationContext) {
+        return value;
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return renderConstant(value.getValue());
+    }
+
+    public static String renderConstant(Object value) {
+        if (value instanceof Long) {
+            return value + "L";
+        }
+        if (value instanceof Float) {
+            return value + "f";
+        }
+        if (value instanceof ParameterizedType) {
+            return ((ParameterizedType)value).getSimpleName() + ".class";
+        }
+        // todo escape string
+        if (value instanceof String) {
+            return "\"" + value + "\"";
+        }
+        return String.valueOf(value);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetElementBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetElementBytecodeExpression.java
new file mode 100644
index 0000000..c0b11bd
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetElementBytecodeExpression.java
@@ -0,0 +1,63 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.InstructionNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ArrayOpCode.getArrayOpCode;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+class GetElementBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression instance;
+    private final BytecodeExpression index;
+    private final InstructionNode arrayLoadInstruction;
+
+    GetElementBytecodeExpression(BytecodeExpression instance, BytecodeExpression index)
+    {
+        super(instance.getType().getArrayComponentType());
+        this.instance = requireNonNull(instance, "instance is null");
+        this.index = requireNonNull(index, "index is null");
+
+        checkArgument(index.getType().getPrimitiveType() == int.class, "index must be int type, but is " + index.getType());
+        this.arrayLoadInstruction = getArrayOpCode(instance.getType().getArrayComponentType()).getLoad();
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(instance.getBytecode(generationContext))
+                .append(index)
+                .append(arrayLoadInstruction);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return instance + "[" + index + "]";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(index);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetFieldBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetFieldBytecodeExpression.java
new file mode 100644
index 0000000..44fb202
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/GetFieldBytecodeExpression.java
@@ -0,0 +1,109 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.FieldDefinition;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+import static com.facebook.presto.bytecode.Access.STATIC;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.getStaticInstruction;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+class GetFieldBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression instance;
+    private final ParameterizedType declaringClass;
+    private final String name;
+
+    GetFieldBytecodeExpression(@Nullable BytecodeExpression instance, Class<?> declaringClass, String name) {
+        this(instance, getDeclaredField(declaringClass, name));
+    }
+
+    GetFieldBytecodeExpression(@Nullable BytecodeExpression instance, Field field) {
+        this(instance, type(requireNonNull(field, "field is null").getDeclaringClass()), field.getName(), type(field.getType()));
+
+        boolean isStatic = Modifier.isStatic(field.getModifiers());
+        if (instance == null) {
+            checkArgument(isStatic, "Field is not static: %s", field);
+        }
+        else {
+            checkArgument(!isStatic, "Field is static: %s", field);
+        }
+    }
+
+    GetFieldBytecodeExpression(@Nullable BytecodeExpression instance, FieldDefinition field) {
+        this(instance, requireNonNull(field, "field is null").getDeclaringClass().getType(), field.getName(), field.getType());
+        if (instance == null) {
+            checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field);
+        }
+        else {
+            checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field);
+        }
+    }
+
+    GetFieldBytecodeExpression(@Nullable BytecodeExpression instance, ParameterizedType declaringClass,
+        String name, ParameterizedType type) {
+        super(type);
+        checkArgument(instance == null || !instance.getType().isPrimitive(), "Type %s does not have fields", getType());
+        this.instance = instance;
+        this.declaringClass = requireNonNull(declaringClass, "declaringClass is null");
+        this.name = requireNonNull(name, "name is null");
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        if (instance == null) {
+            return getStaticInstruction(declaringClass, name, getType());
+        }
+
+        return new BytecodeBlock()
+            .append(instance.getBytecode(generationContext))
+            .getField(declaringClass, name, getType());
+    }
+
+    @Override
+    protected String formatOneLine() {
+        if (instance == null) {
+            return declaringClass.getSimpleName() + "." + name;
+        }
+        return instance + "." + name;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return (instance == null) ? List.of() : List.of(instance);
+    }
+
+    private static Field getDeclaredField(Class<?> declaringClass, String name) {
+        requireNonNull(declaringClass, "declaringClass is null");
+        requireNonNull(name, "name is null");
+
+        try {
+            return declaringClass.getField(name);
+        }
+        catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(format("Class %s does not have a '%s' field", declaringClass.getName(), name));
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InlineIfBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InlineIfBytecodeExpression.java
new file mode 100644
index 0000000..1ab706e
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InlineIfBytecodeExpression.java
@@ -0,0 +1,69 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+class InlineIfBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression condition;
+    private final BytecodeExpression ifTrue;
+    private final BytecodeExpression ifFalse;
+
+    InlineIfBytecodeExpression(BytecodeExpression condition, BytecodeExpression ifTrue, BytecodeExpression ifFalse)
+    {
+        super(ifTrue.getType());
+        this.condition = condition;
+        this.ifTrue = requireNonNull(ifTrue, "ifTrue is null");
+        this.ifFalse = requireNonNull(ifFalse, "ifFalse is null");
+
+        checkArgument(condition.getType().getPrimitiveType() == boolean.class, "Expected condition to be type boolean but is %s", condition.getType());
+        checkArgument(ifTrue.getType().equals(ifFalse.getType()), "Expected ifTrue and ifFalse to be the same type");
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        LabelNode falseLabel = new LabelNode("false");
+        LabelNode endLabel = new LabelNode("end");
+        return new BytecodeBlock()
+                .append(condition)
+                .ifFalseGoto(falseLabel)
+                .append(ifTrue)
+                .gotoLabel(endLabel)
+                .visitLabel(falseLabel)
+                .append(ifFalse)
+                .visitLabel(endLabel);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(condition, ifTrue, ifFalse);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "(" + condition + " ? " + ifTrue + " : " + ifFalse + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InstanceOfBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InstanceOfBytecodeExpression.java
new file mode 100644
index 0000000..39ed17c
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InstanceOfBytecodeExpression.java
@@ -0,0 +1,62 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class InstanceOfBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression instance;
+    private final Class<?> type;
+
+    InstanceOfBytecodeExpression(BytecodeExpression instance, Class<?> type)
+    {
+        super(type(boolean.class));
+
+        this.instance = requireNonNull(instance, "instance is null");
+        this.type = requireNonNull(type, "type is null");
+    }
+
+    public static BytecodeExpression instanceOf(BytecodeExpression instance, Class<?> type)
+    {
+        return new InstanceOfBytecodeExpression(instance, type);
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(instance)
+                .isInstanceOf(type);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return instance + " instanceof " + type;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeBytecodeExpression.java
new file mode 100644
index 0000000..f80ed6b
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeBytecodeExpression.java
@@ -0,0 +1,115 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static java.util.Objects.requireNonNull;
+
+class InvokeBytecodeExpression
+    extends BytecodeExpression {
+    public static InvokeBytecodeExpression createInvoke(
+        BytecodeExpression instance,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        return new InvokeBytecodeExpression(
+            requireNonNull(instance, "instance is null"),
+            instance.getType(),
+            requireNonNull(methodName, "methodName is null"),
+            requireNonNull(returnType, "returnType is null"),
+            requireNonNull(parameterTypes, "parameterTypes is null"),
+            requireNonNull(parameters, "parameters is null"));
+    }
+
+    @Nullable
+    private final BytecodeExpression instance;
+    private final ParameterizedType methodTargetType;
+    private final String methodName;
+    private final ParameterizedType returnType;
+    private final List<BytecodeExpression> parameters;
+    private final List<ParameterizedType> parameterTypes;
+
+    InvokeBytecodeExpression(
+        @Nullable BytecodeExpression instance,
+        ParameterizedType methodTargetType,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<ParameterizedType> parameterTypes,
+        Collection<? extends BytecodeExpression> parameters) {
+        super(requireNonNull(returnType, "returnType is null"));
+        checkArgument(instance == null || !instance.getType().isPrimitive(), "Type %s does not have methods", getType());
+        this.instance = instance;
+        this.methodTargetType = requireNonNull(methodTargetType, "methodTargetType is null");
+        this.methodName = requireNonNull(methodName, "methodName is null");
+        this.returnType = returnType;
+        this.parameterTypes = List.copyOf(requireNonNull(parameterTypes, "parameterTypes is null"));
+        this.parameters = List.copyOf(requireNonNull(parameters, "parameters is null"));
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        BytecodeBlock block = new BytecodeBlock();
+        if (instance != null) {
+            block.append(instance);
+        }
+
+        for (BytecodeExpression parameter : parameters) {
+            block.append(parameter);
+        }
+
+        if (instance == null) {
+            return block.invokeStatic(methodTargetType, methodName, returnType, parameterTypes);
+        }
+        else if (instance.getType().isInterface()) {
+            return block.invokeInterface(methodTargetType, methodName, returnType, parameterTypes);
+        }
+        else {
+            return block.invokeVirtual(methodTargetType, methodName, returnType, parameterTypes);
+        }
+    }
+
+    @Override
+    protected String formatOneLine() {
+        if (instance == null) {
+            return methodTargetType.getSimpleName() + "." + methodName + "(" +
+                parameters.stream().map(BytecodeExpression::toString).collect(Collectors.joining(", ")) + ")";
+        }
+
+        return instance + "." + methodName + "(" +
+            parameters.stream().map(BytecodeExpression::toString).collect(Collectors.joining(", ")) + ")";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        if (instance == null)
+            return List.copyOf(parameters);
+
+        final ArrayList<BytecodeNode> children = new ArrayList<>(parameters.size() + 1);
+        children.add(instance);
+        children.addAll(parameters);
+        return Collections.unmodifiableList(children);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeDynamicBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeDynamicBytecodeExpression.java
new file mode 100644
index 0000000..d8ff52c
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/InvokeDynamicBytecodeExpression.java
@@ -0,0 +1,84 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
+class InvokeDynamicBytecodeExpression
+    extends BytecodeExpression {
+    private final Method bootstrapMethod;
+    private final List<Object> bootstrapArgs;
+    private final String methodName;
+    private final ParameterizedType returnType;
+    private final List<BytecodeExpression> parameters;
+    private final List<ParameterizedType> parameterTypes;
+
+    InvokeDynamicBytecodeExpression(
+        Method bootstrapMethod,
+        Collection<?> bootstrapArgs,
+        String methodName,
+        ParameterizedType returnType,
+        Collection<? extends BytecodeExpression> parameters,
+        Collection<ParameterizedType> parameterTypes
+    ) {
+        super(returnType);
+        this.bootstrapMethod = requireNonNull(bootstrapMethod, "bootstrapMethod is null");
+        this.bootstrapArgs = List.copyOf(requireNonNull(bootstrapArgs, "bootstrapArgs is null"));
+        this.methodName = requireNonNull(methodName, "methodName is null");
+        this.returnType = requireNonNull(returnType, "returnType is null");
+        this.parameters = List.copyOf(requireNonNull(parameters, "parameters is null"));
+        this.parameterTypes = List.copyOf(requireNonNull(parameterTypes, "parameterTypes is null"));
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        BytecodeBlock block = new BytecodeBlock();
+        for (BytecodeExpression parameter : parameters) {
+            block.append(parameter);
+        }
+        return block.invokeDynamic(methodName, returnType, parameterTypes, bootstrapMethod, bootstrapArgs);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    protected String formatOneLine() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("[").append(bootstrapMethod.getName());
+        if (!bootstrapArgs.isEmpty()) {
+            builder.append("(").append(bootstrapArgs.stream().map(ConstantBytecodeExpression::renderConstant)
+                .collect(Collectors.joining(", "))).append(")");
+        }
+        builder.append("]=>");
+
+        builder.append(methodName)
+            .append("(")
+            .append(parameters.stream().map(BytecodeExpression::toString).collect(Collectors.joining(", ")))
+            .append(")");
+
+        return builder.toString();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NegateBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NegateBytecodeExpression.java
new file mode 100644
index 0000000..f1873c5
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NegateBytecodeExpression.java
@@ -0,0 +1,66 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.OpCode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.expression.ArithmeticBytecodeExpression.getNumericOpCode;
+import static java.util.Objects.requireNonNull;
+
+class NegateBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression value;
+    private final OpCode negateOpCode;
+
+    NegateBytecodeExpression(BytecodeExpression value)
+    {
+        super(requireNonNull(value, "value is null").getType());
+        this.value = value;
+
+        Class<?> type = value.getType().getPrimitiveType();
+        checkArgument(type != null, "value is not a primitive");
+        checkArgument(type != void.class, "value is void");
+        checkArgument(type == int.class || type == long.class || type == float.class || type == double.class,
+                "value argument must be int, long, float, or double, but is %s",
+                type);
+
+        negateOpCode = getNumericOpCode("Negate", OpCode.INEG, type);
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        return new BytecodeBlock()
+                .append(value)
+                .append(negateOpCode);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(value);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "-(" + value + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewArrayBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewArrayBytecodeExpression.java
new file mode 100644
index 0000000..7e2c25a
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewArrayBytecodeExpression.java
@@ -0,0 +1,99 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import com.facebook.presto.bytecode.instruction.TypeInstruction;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.jetbrains.annotations.Nullable;
+
+import static com.facebook.presto.bytecode.ArrayOpCode.getArrayOpCode;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.expression.BytecodeExpressions.constantInt;
+import static java.util.Objects.requireNonNull;
+
+class NewArrayBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression length;
+    private final ParameterizedType elementType;
+
+    @Nullable
+    private final List<BytecodeExpression> elements;
+
+    NewArrayBytecodeExpression(ParameterizedType type, int length) {
+        this(type, constantInt(length));
+    }
+
+    NewArrayBytecodeExpression(ParameterizedType type, BytecodeExpression length) {
+        this(type, length, null);
+    }
+
+    NewArrayBytecodeExpression(ParameterizedType type, Collection<BytecodeExpression> elements) {
+        this(type, constantInt(elements.size()), elements);
+    }
+
+    private NewArrayBytecodeExpression(ParameterizedType type, BytecodeExpression length,
+        Collection<BytecodeExpression> elements) {
+        super(type);
+        requireNonNull(type, "type is null");
+        checkArgument(type.getArrayComponentType() != null, "type %s must be array type", type);
+        this.elementType = type.getArrayComponentType();
+        this.length = requireNonNull(length, "length is null");
+        this.elements = (elements == null) ? null : List.copyOf(elements);
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        BytecodeBlock bytecodeBlock;
+        if (elementType.isPrimitive()) {
+            bytecodeBlock = new BytecodeBlock()
+                .append(length)
+                .append(TypeInstruction.newPrimitiveArray(elementType));
+        }
+        else {
+            bytecodeBlock = new BytecodeBlock()
+                .append(length)
+                .append(TypeInstruction.newObjectArray(elementType));
+        }
+        if (elements != null) {
+            for (int i = 0; i < elements.size(); i++) {
+                BytecodeExpression element = elements.get(i);
+                bytecodeBlock
+                    .dup()
+                    .append(constantInt(i))
+                    .append(element)
+                    .append(getArrayOpCode(elementType).getStore());
+            }
+        }
+        return bytecodeBlock;
+    }
+
+    @Override
+    protected String formatOneLine() {
+        if (elements == null) {
+            return "new " + elementType.getSimpleName() + "[" + length + "]";
+        }
+        return "new " + elementType.getSimpleName() + "[] {" + elements.stream().map(BytecodeExpression::toString).collect(Collectors.joining(", ")) + "}";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewInstanceBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewInstanceBytecodeExpression.java
new file mode 100644
index 0000000..53c09b7
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NewInstanceBytecodeExpression.java
@@ -0,0 +1,67 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static java.util.Objects.requireNonNull;
+
+class NewInstanceBytecodeExpression
+        extends BytecodeExpression
+{
+    private final List<BytecodeExpression> parameters;
+    private final List<ParameterizedType> parameterTypes;
+
+    NewInstanceBytecodeExpression(
+            ParameterizedType type,
+            Collection<ParameterizedType> parameterTypes,
+            Collection<? extends BytecodeExpression> parameters)
+    {
+        super(type);
+        this.parameterTypes = List.copyOf(requireNonNull(parameterTypes, "parameterTypes is null"));
+        this.parameters = List.copyOf(requireNonNull(parameters, "parameters is null"));
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        BytecodeBlock block = new BytecodeBlock()
+                .newObject(getType())
+                .dup();
+
+        for (BytecodeExpression parameter : parameters) {
+            block.append(parameter);
+        }
+        return block.invokeConstructor(getType(), parameterTypes);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "new " + getType().getSimpleName() + "(" +
+            parameters.stream().map(BytecodeExpression::toString).collect(Collectors.joining(", ")) + ")";
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.copyOf(parameters);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NotBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NotBytecodeExpression.java
new file mode 100644
index 0000000..ed8d7ae
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/NotBytecodeExpression.java
@@ -0,0 +1,63 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+
+class NotBytecodeExpression
+        extends BytecodeExpression
+{
+    private final BytecodeExpression value;
+
+    NotBytecodeExpression(BytecodeExpression value)
+    {
+        super(type(boolean.class));
+        this.value = value;
+        checkArgument(value.getType().getPrimitiveType() == boolean.class, "Expected value to be type boolean but is %s", value.getType());
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext)
+    {
+        LabelNode trueLabel = new LabelNode("true");
+        LabelNode endLabel = new LabelNode("end");
+        return new BytecodeBlock()
+                .append(value)
+                .ifTrueGoto(trueLabel)
+                .push(true)
+                .gotoLabel(endLabel)
+                .visitLabel(trueLabel)
+                .push(false)
+                .visitLabel(endLabel);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes()
+    {
+        return List.of(value);
+    }
+
+    @Override
+    protected String formatOneLine()
+    {
+        return "(!" + value + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/OrBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/OrBytecodeExpression.java
new file mode 100644
index 0000000..25dfa79
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/OrBytecodeExpression.java
@@ -0,0 +1,64 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.instruction.LabelNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class OrBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression left;
+    private final BytecodeExpression right;
+
+    OrBytecodeExpression(BytecodeExpression left, BytecodeExpression right) {
+        super(type(boolean.class));
+        this.left = requireNonNull(left, "left is null");
+        checkArgument(left.getType().getPrimitiveType() == boolean.class, "Expected left to be type boolean but is %s", left.getType());
+        this.right = requireNonNull(right, "right is null");
+        checkArgument(right.getType().getPrimitiveType() == boolean.class, "Expected right to be type boolean but is %s", right.getType());
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        LabelNode trueLabel = new LabelNode("true");
+        LabelNode endLabel = new LabelNode("end");
+        return new BytecodeBlock()
+            .append(left)
+            .ifTrueGoto(trueLabel)
+            .append(right)
+            .ifTrueGoto(trueLabel)
+            .push(false)
+            .gotoLabel(endLabel)
+            .visitLabel(trueLabel)
+            .push(true)
+            .visitLabel(endLabel);
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(left, right);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return "(" + left + " || " + right + ")";
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/PopBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/PopBytecodeExpression.java
new file mode 100644
index 0000000..c00cb32
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/PopBytecodeExpression.java
@@ -0,0 +1,49 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class PopBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression instance;
+
+    PopBytecodeExpression(BytecodeExpression instance) {
+        super(type(void.class));
+        this.instance = requireNonNull(instance, "instance is null");
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        return new BytecodeBlock()
+            .append(instance.getBytecode(generationContext))
+            .pop(instance.getType());
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return instance.toString();
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(instance);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ReturnBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ReturnBytecodeExpression.java
new file mode 100644
index 0000000..8066509
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/ReturnBytecodeExpression.java
@@ -0,0 +1,82 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.OpCode;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class ReturnBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression instance;
+    private final OpCode returnOpCode;
+
+    ReturnBytecodeExpression(BytecodeExpression instance) {
+        super(type(void.class));
+        this.instance = requireNonNull(instance, "instance is null");
+        this.returnOpCode = returnOpCode(instance.getType());
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        return new BytecodeBlock()
+            .append(instance.getBytecode(generationContext))
+            .append(returnOpCode);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return "return " + instance;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(instance);
+    }
+
+    private static OpCode returnOpCode(ParameterizedType componentType) {
+        Class<?> primitiveType = componentType.getPrimitiveType();
+        if (primitiveType != null) {
+            if (primitiveType == byte.class ||
+                primitiveType == boolean.class ||
+                primitiveType == char.class ||
+                primitiveType == short.class ||
+                primitiveType == int.class) {
+                return OpCode.IRETURN;
+            }
+            if (primitiveType == long.class) {
+                return OpCode.LRETURN;
+            }
+            if (primitiveType == float.class) {
+                return OpCode.FRETURN;
+            }
+            if (primitiveType == double.class) {
+                return OpCode.DRETURN;
+            }
+            if (primitiveType == void.class) {
+                return OpCode.RETURN;
+            }
+            throw new IllegalArgumentException("Unsupported array type: " + primitiveType);
+        }
+        else {
+            return OpCode.ARETURN;
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetArrayElementBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetArrayElementBytecodeExpression.java
new file mode 100644
index 0000000..06406f9
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetArrayElementBytecodeExpression.java
@@ -0,0 +1,68 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import com.facebook.presto.bytecode.instruction.InstructionNode;
+import java.util.List;
+
+import static com.facebook.presto.bytecode.ArrayOpCode.getArrayOpCode;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.util.Objects.requireNonNull;
+
+class SetArrayElementBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression instance;
+    private final BytecodeExpression index;
+    private final BytecodeExpression value;
+    private final InstructionNode arrayStoreInstruction;
+
+    SetArrayElementBytecodeExpression(BytecodeExpression instance, BytecodeExpression index,
+        BytecodeExpression value) {
+        super(type(void.class));
+
+        this.instance = requireNonNull(instance, "instance is null");
+        this.index = requireNonNull(index, "index is null");
+        this.value = requireNonNull(value, "value is null");
+
+        ParameterizedType componentType = instance.getType().getArrayComponentType();
+        checkArgument(index.getType().getPrimitiveType() == int.class, "index must be int type, but is " + index.getType());
+        checkArgument(componentType.equals(value.getType()), "value must be %s type, but is %s", componentType, value.getType());
+
+        this.arrayStoreInstruction = getArrayOpCode(componentType).getStore();
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        return new BytecodeBlock()
+            .append(instance.getBytecode(generationContext))
+            .append(index)
+            .append(value)
+            .append(arrayStoreInstruction);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        return instance + "[" + index + "] = " + value;
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of(index, value);
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetFieldBytecodeExpression.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetFieldBytecodeExpression.java
new file mode 100644
index 0000000..f1dc2c4
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/expression/SetFieldBytecodeExpression.java
@@ -0,0 +1,129 @@
+/*
+ * 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.facebook.presto.bytecode.expression;
+
+import com.facebook.presto.bytecode.BytecodeBlock;
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.FieldDefinition;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import org.jetbrains.annotations.Nullable;
+
+import static com.facebook.presto.bytecode.Access.STATIC;
+import static com.facebook.presto.bytecode.BytecodeUtils.checkArgument;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+class SetFieldBytecodeExpression
+    extends BytecodeExpression {
+    private final BytecodeExpression instance;
+    private final ParameterizedType declaringClass;
+    private final String name;
+    private final BytecodeExpression value;
+    private final ParameterizedType fieldType;
+
+    SetFieldBytecodeExpression(@Nullable BytecodeExpression instance, Class<?> declaringClass, String name,
+        BytecodeExpression value) {
+        this(instance, getDeclaredField(declaringClass, name), value);
+    }
+
+    SetFieldBytecodeExpression(@Nullable BytecodeExpression instance, Field field, BytecodeExpression value) {
+        this(instance, type(requireNonNull(field, "field is null").getDeclaringClass()), field.getName(), value, type(field.getType()));
+
+        boolean isStatic = Modifier.isStatic(field.getModifiers());
+        if (instance == null) {
+            checkArgument(isStatic, "Field is not static: %s", field);
+        }
+        else {
+            checkArgument(!isStatic, "Field is static: %s", field);
+        }
+    }
+
+    SetFieldBytecodeExpression(@Nullable BytecodeExpression instance, FieldDefinition field,
+        BytecodeExpression value) {
+        this(instance, requireNonNull(field, "field is null").getDeclaringClass().getType(), field.getName(), value, field.getType());
+        if (instance == null) {
+            checkArgument(field.getAccess().contains(STATIC), "Field is not static: %s", field);
+        }
+        else {
+            checkArgument(!field.getAccess().contains(STATIC), "Field is static: %s", field);
+        }
+    }
+
+    SetFieldBytecodeExpression(@Nullable BytecodeExpression instance, ParameterizedType declaringClass,
+        String name, BytecodeExpression value) {
+        this(instance, declaringClass, name, value, value.getType());
+    }
+
+    SetFieldBytecodeExpression(@Nullable BytecodeExpression instance,
+        ParameterizedType declaringClass,
+        String name,
+        BytecodeExpression value,
+        ParameterizedType fieldType) {
+        super(type(void.class));
+        if (instance != null) {
+            checkArgument(!instance.getType().isPrimitive(), "Type %s does not have fields", instance.getType());
+        }
+        this.instance = instance;
+        this.declaringClass = requireNonNull(declaringClass, "declaringClass is null");
+        this.name = requireNonNull(name, "name is null");
+        this.fieldType = requireNonNull(fieldType, "fieldType is null");
+        this.value = requireNonNull(value, "value is null");
+    }
+
+    @Override
+    public BytecodeNode getBytecode(MethodGenerationContext generationContext) {
+        if (instance == null) {
+            return new BytecodeBlock()
+                .append(value.getBytecode(generationContext))
+                .putStaticField(declaringClass, name, fieldType);
+        }
+
+        return new BytecodeBlock()
+            .append(instance.getBytecode(generationContext))
+            .append(value.getBytecode(generationContext))
+            .putField(declaringClass, name, fieldType);
+    }
+
+    @Override
+    protected String formatOneLine() {
+        if (instance == null) {
+            return declaringClass.getSimpleName() + "." + name + " = " + value;
+        }
+        else {
+            return instance + "." + name + " = " + value;
+        }
+    }
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return instance != null ? List.of(instance, value) : List.of(value);
+    }
+
+    private static Field getDeclaredField(Class<?> declaringClass, String name) {
+        requireNonNull(declaringClass, "declaringClass is null");
+        requireNonNull(name, "name is null");
+
+        try {
+            return declaringClass.getField(name);
+        }
+        catch (NoSuchFieldException e) {
+            throw new IllegalArgumentException(format("Class %s does not have a '%s' field", declaringClass.getName(), name));
+        }
+    }
+}
diff --git a/modules/bytecode/src/main/java/com/facebook/presto/bytecode/instruction/Constant.java b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/instruction/Constant.java
new file mode 100644
index 0000000..abecbc8
--- /dev/null
+++ b/modules/bytecode/src/main/java/com/facebook/presto/bytecode/instruction/Constant.java
@@ -0,0 +1,542 @@
+/*
+ * 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.facebook.presto.bytecode.instruction;
+
+import com.facebook.presto.bytecode.BytecodeNode;
+import com.facebook.presto.bytecode.BytecodeUtils;
+import com.facebook.presto.bytecode.BytecodeVisitor;
+import com.facebook.presto.bytecode.MethodGenerationContext;
+import com.facebook.presto.bytecode.ParameterizedType;
+import java.util.List;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Type;
+
+import static com.facebook.presto.bytecode.OpCode.ACONST_NULL;
+import static com.facebook.presto.bytecode.OpCode.BIPUSH;
+import static com.facebook.presto.bytecode.OpCode.DCONST_0;
+import static com.facebook.presto.bytecode.OpCode.DCONST_1;
+import static com.facebook.presto.bytecode.OpCode.FCONST_0;
+import static com.facebook.presto.bytecode.OpCode.FCONST_1;
+import static com.facebook.presto.bytecode.OpCode.FCONST_2;
+import static com.facebook.presto.bytecode.OpCode.ICONST_0;
+import static com.facebook.presto.bytecode.OpCode.ICONST_1;
+import static com.facebook.presto.bytecode.OpCode.ICONST_2;
+import static com.facebook.presto.bytecode.OpCode.ICONST_3;
+import static com.facebook.presto.bytecode.OpCode.ICONST_4;
+import static com.facebook.presto.bytecode.OpCode.ICONST_5;
+import static com.facebook.presto.bytecode.OpCode.ICONST_M1;
+import static com.facebook.presto.bytecode.OpCode.LCONST_0;
+import static com.facebook.presto.bytecode.OpCode.LCONST_1;
+import static com.facebook.presto.bytecode.OpCode.SIPUSH;
+import static com.facebook.presto.bytecode.ParameterizedType.type;
+import static com.facebook.presto.bytecode.instruction.FieldInstruction.getStaticInstruction;
+import static com.facebook.presto.bytecode.instruction.InvokeInstruction.invokeStatic;
+import static java.util.Objects.requireNonNull;
+
+@SuppressWarnings("UnusedDeclaration")
+public abstract class Constant
+    implements InstructionNode {
+    public static Constant loadNull() {
+        return new NullConstant();
+    }
+
+    public static Constant loadBoolean(boolean value) {
+        return new BooleanConstant(value);
+    }
+
+    public static Constant loadBoxedBoolean(boolean value) {
+        return new BoxedBooleanConstant(value);
+    }
+
+    public static Constant loadInt(int value) {
+        return new IntConstant(value);
+    }
+
+    public static Constant loadBoxedInt(int value) {
+        return new BoxedIntegerConstant(value);
+    }
+
+    public static Constant loadFloat(float value) {
+        return new FloatConstant(value);
+    }
+
+    public static Constant loadBoxedFloat(float value) {
+        return new BoxedFloatConstant(value);
+    }
+
+    public static Constant loadLong(long value) {
+        return new LongConstant(value);
+    }
+
+    public static Constant loadBoxedLong(long value) {
+        return new BoxedLongConstant(value);
+    }
+
+    public static Constant loadDouble(double value) {
+        return new DoubleConstant(value);
+    }
+
+    public static Constant loadBoxedDouble(double value) {
+        return new BoxedDoubleConstant(value);
+    }
+
+    public static Constant loadNumber(Number value) {
+        requireNonNull(value, "value is null");
+        if (value instanceof Byte) {
+            return loadInt((value).intValue());
+        }
+        if (value instanceof Short) {
+            return loadInt((value).intValue());
+        }
+        if (value instanceof Integer) {
+            return loadInt((Integer)value);
+        }
+        if (value instanceof Long) {
+            return loadLong((Long)value);
+        }
+        if (value instanceof Float) {
+            return loadFloat((Float)value);
+        }
+        if (value instanceof Double) {
+            return loadDouble((Double)value);
+        }
+        throw new IllegalStateException("Unsupported number type " + value.getClass().getSimpleName());
+    }
+
+    public static Constant loadString(String value) {
+        requireNonNull(value, "value is null");
+        return new StringConstant(value);
+    }
+
+    public static Constant loadClass(Class<?> value) {
+        requireNonNull(value, "value is null");
+        return new ClassConstant(type(value));
+    }
+
+    public static Constant loadClass(ParameterizedType value) {
+        requireNonNull(value, "value is null");
+        return new ClassConstant(value);
+    }
+
+    public abstract Object getValue();
+
+    @Override
+    public List<BytecodeNode> getChildNodes() {
+        return List.of();
+    }
+
+    @Override
+    public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+        return visitor.visitConstant(parent, this);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "{value=" + getValue() + '}';
+    }
+
+    public static class NullConstant
+        extends Constant {
+        @Override
+        public Object getValue() {
+            return null;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            visitor.visitInsn(ACONST_NULL.getOpCode());
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitConstant(parent, this);
+        }
+    }
+
+    public static class BooleanConstant
+        extends Constant {
+        private final boolean value;
+
+        private BooleanConstant(boolean value) {
+            this.value = value;
+        }
+
+        @Override
+        public Boolean getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            if (value) {
+                visitor.visitInsn(ICONST_1.getOpCode());
+            }
+            else {
+                visitor.visitInsn(ICONST_0.getOpCode());
+            }
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitBooleanConstant(parent, this);
+        }
+    }
+
+    public static class BoxedBooleanConstant
+        extends Constant {
+        private final boolean value;
+
+        private BoxedBooleanConstant(boolean value) {
+            this.value = value;
+        }
+
+        @Override
+        public Boolean getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            if (value) {
+                getStaticInstruction(Boolean.class, "TRUE", Boolean.class).accept(visitor, generationContext);
+            }
+            else {
+                getStaticInstruction(Boolean.class, "FALSE", Boolean.class).accept(visitor, generationContext);
+            }
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitBoxedBooleanConstant(parent, this);
+        }
+    }
+
+    public static class IntConstant
+        extends Constant {
+        private final int value;
+
+        private IntConstant(int value) {
+            this.value = value;
+        }
+
+        @Override
+        public Integer getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            if (value <= Byte.MAX_VALUE && value >= Byte.MIN_VALUE) {
+                switch (value) {
+                    case -1:
+                        visitor.visitInsn(ICONST_M1.getOpCode());
+                        break;
+                    case 0:
+                        visitor.visitInsn(ICONST_0.getOpCode());
+                        break;
+                    case 1:
+                        visitor.visitInsn(ICONST_1.getOpCode());
+                        break;
+                    case 2:
+                        visitor.visitInsn(ICONST_2.getOpCode());
+                        break;
+                    case 3:
+                        visitor.visitInsn(ICONST_3.getOpCode());
+                        break;
+                    case 4:
+                        visitor.visitInsn(ICONST_4.getOpCode());
+                        break;
+                    case 5:
+                        visitor.visitInsn(ICONST_5.getOpCode());
+                        break;
+                    default:
+                        visitor.visitIntInsn(BIPUSH.getOpCode(), value);
+                        break;
+                }
+            }
+            else if (value <= Short.MAX_VALUE && value >= Short.MIN_VALUE) {
+                visitor.visitIntInsn(SIPUSH.getOpCode(), value);
+            }
+            else {
+                visitor.visitLdcInsn(value);
+            }
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitIntConstant(parent, this);
+        }
+    }
+
+    public static class BoxedIntegerConstant
+        extends Constant {
+        private final int value;
+
+        private BoxedIntegerConstant(int value) {
+            this.value = value;
+        }
+
+        @Override
+        public Integer getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            loadInt(value).accept(visitor, generationContext);
+            invokeStatic(Integer.class, "valueOf", Integer.class, int.class).accept(visitor, generationContext);
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitBoxedIntegerConstant(parent, this);
+        }
+    }
+
+    public static class FloatConstant
+        extends Constant {
+        private final float value;
+
+        private FloatConstant(float value) {
+            this.value = value;
+        }
+
+        @Override
+        public Float getValue() {
+            return value;
+        }
+
+        @Override
+        @SuppressWarnings("FloatingPointEquality")
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            // We can not use "value == 0.0" because when value is "-0.0" the expression
+            // will evaluate to true and we would convert "-0.0" to "0.0" which is
+            // not the same value
+            if (Float.floatToIntBits(value) == Float.floatToIntBits(0.0f)) {
+                visitor.visitInsn(FCONST_0.getOpCode());
+            }
+            else if (value == 1.0f) {
+                visitor.visitInsn(FCONST_1.getOpCode());
+            }
+            else if (value == 2.0f) {
+                visitor.visitInsn(FCONST_2.getOpCode());
+            }
+            else {
+                visitor.visitLdcInsn(value);
+            }
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitFloatConstant(parent, this);
+        }
+    }
+
+    public static class BoxedFloatConstant
+        extends Constant {
+        private final float value;
+
+        private BoxedFloatConstant(float value) {
+            this.value = value;
+        }
+
+        @Override
+        public Float getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            loadFloat(value).accept(visitor, generationContext);
+            invokeStatic(Float.class, "valueOf", Float.class, float.class).accept(visitor, generationContext);
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitBoxedFloatConstant(parent, this);
+        }
+    }
+
+    public static class LongConstant
+        extends Constant {
+        private final long value;
+
+        private LongConstant(long value) {
+            this.value = value;
+        }
+
+        @Override
+        public Long getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            if (value == 0) {
+                visitor.visitInsn(LCONST_0.getOpCode());
+            }
+            else if (value == 1) {
+                visitor.visitInsn(LCONST_1.getOpCode());
+            }
+            else {
+                visitor.visitLdcInsn(value);
+            }
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
+            return visitor.visitLongConstant(parent, this);
+        }
+    }
+
+    public static class BoxedLongConstant
+        extends Constant {
+        private final long value;
+
+        private BoxedLongConstant(long value) {
+            this.value = value;
+        }
+
+        @Override
+        public Long getValue() {
+            return value;
+        }
+
+        @Override
+        public void accept(MethodVisitor visitor, MethodGenerationContext generationContext) {
+            loadLong(value).accept(visitor, generationContext);
+            invokeStatic(Long.class, "valueOf", Long.class, long.class).accept(visitor, generationContext);
+        }
+
+        @Override
+        public <T> T accept(BytecodeNode parent, BytecodeVisitor<T> visitor) {
... 10568 lines suppressed ...