Skip to content

Expressions Framework

Mike Strobel edited this page Oct 27, 2019 · 1 revision

Overview

The procyon-expressions framework enables runtime code generation using declarative expression trees. It is almost a direct port of .NET's LINQ/DLR expression trees API (System.Linq.Expressions), but without dynamic callsite support. procyon-expressions frees the developer from having to emit raw bytecode, relying instead on a composition model whose shape more closely resembles actual code.

All Top-Level Expressions are Lambdas

While the procyon-expressions API may loosely resemble a Java AST, it is not such. You cannot declare types or methods; the "compilation unit" of this framework is LambdaExpression. A lambda expression is an invokable expression; its body may be a trivial expression or a block of expressions with complex control flow. A lambda may be compiled to an instance of a function interface (a single-method interface) or to a MethodHandle. Alternatively, it may compile directly to a MethodBuilder, allowing developers to implement method bodies using expression trees instead of emitting bytecode directly.

All lambda expressions have a return type, even if it is void. They may have one or more parameters, and they may emit closures in order to reference complex constants which cannot be emitted directly in bytecode (e.g., references to objects, arrays, locals or parameters of an outer lambda expression, etc.). Note that lambdas which compile to a MethodBuilder cannot utilize closures, and as such cannot reference complex constants.

Types of Expressions

Binary Expressions

Procyon supports most standard binary operators, including all of those supported by the Java language. These include arithmetic operators (add, subtract, multiply, divide, etc.), logical operators (andAlso, orElse), as well as bitwise operators (and, or, xor, etc.).

Each operator has its own factory method(s) in the Expressions class, named according to the operations they perform. For instance:

:::java
public static BinaryExpression add(
    final Expression left,
    final Expression right);

public static BinaryExpression add(
    final Expression left,
    final Expression right,
    final MethodInfo method);

public static BinaryExpression addAssign(
    final Expression left,
    final Expression right);

public static BinaryExpression addAssign(
    final Expression left,
    final Expression right,
    final MethodInfo method);

There is also a series of makeBinary methods which can be used to compose binary expressions with the operator type taken as an operand.

:::java
public static BinaryExpression makeBinary(
    final ExpressionType binaryType,
    final Expression left,
    final Expression right);

public static BinaryExpression makeBinary(
    final ExpressionType binaryType,
    final Expression left,
    final Expression right,
    final MethodInfo method);

public static BinaryExpression makeBinary(
    final ExpressionType binaryType,
    final Expression first,
    final Expression... rest);

Notice that many of these factory methods accept a MethodInfo as an argument. This effectively allows for user-defined operators by specifying a method to perform the operation. The method may be a static or instance method, though static methods are recommended to avoid a NullPointerException if the left operand is null.

One useful binary expression type not supported by the Java language is the coalesce operator, which returns the left operand if it is non-null, and returns the right operand otherwise.

:::java
public static BinaryExpression coalesce(final Expression left, final Expression right)

Note that String concatenation uses the concat operator rather than the add operator.

:::java
public static ConcatExpression concat(final Expression first, final Expression second)

Note also that logical and bitwise operators with similar names are differentiated. In Procyon, the and and or operators refer to bitwise operations (i.e., & and |) while andAlso and orElse represent logical expressions (i.e., && and ||). Be careful not to confuse these; while the behavior may appear similar, the bitwise operators do not exhibit the short circuiting behavior of the logical operators.

Block Expressions

Block expressions may contain multiple child expressions and provide a scope for the declaration of local variables. Variables declared within a BlockExpression may only be referenced from within the block or one of its descendant expressions.

If a block's type is not specified explicitly, then its type will be that of its last child expression. Therefore, when using a BlockExpression in places where a void-typed expression is expected, it may be necessary to explicitly specify the block's type (and is probably good practice even if not strictly required).

:::java
public static BlockExpression block(final Expression... expressions);
public static BlockExpression block(final ExpressionList<? extends Expression> expressions);

public static BlockExpression block(
    final ParameterExpression[] variables,
    final Expression... expressions);

public static BlockExpression block(
    final ParameterExpressionList variables,
    final Expression... expressions);

public static BlockExpression block(
    final ParameterExpression[] variables,
    final ExpressionList<? extends Expression> expressions);

public static BlockExpression block(
    final Type type,
    final Expression... expressions);

public static BlockExpression block(
    final Type type,
    final ExpressionList<? extends Expression> expressions);

public static BlockExpression block(
    final Type type,
    final ParameterExpression[] variables,
    final Expression... expressions);

public static BlockExpression block(
    final Type type,
    final ParameterExpressionList variables,
    final Expression... expressions);

public static BlockExpression block(
    final Type type,
    final ParameterExpression[] variables,
    final ExpressionList<? extends Expression> expressions);

Call Expressions

A MethodCallExpression represents a method call, and it may include an optional target expression (for instance methods) and zero or more parameter expressions. The exact method to invoke may be specified via a MethodInfo parameter, or the method name and type arguments may be provided, and the method resolved using the framework's default binder.

:::java
public static MethodCallExpression call(
    final MethodInfo method,
    final Expression... arguments);

public static MethodCallExpression call(
    final Expression target,
    final MethodInfo method,
    final Expression... arguments);

public static MethodCallExpression call(
    final Expression target,
    final String methodName,
    final Expression... arguments);

public static MethodCallExpression call(
    final Expression target,
    final String methodName,
    final TypeList typeArguments,
    final Expression... arguments);

public static MethodCallExpression call(
    final Type declaringType,
    final String methodName,
    final Expression... arguments);

public static MethodCallExpression call(
    final Type declaringType,
    final String methodName,
    final TypeList typeArguments,
    final Expression... arguments);

Conditional Expressions

Conditional expressions consist of a boolean-typed test expression and two operands. If the test expression evaluates to true at runtime, the first operand will be evaluated ("returned"). Otherwise, the second operand will be evaluated. A ConditionalExpression resembles the "ternary" (?:) conditional operator in Java. A conditional expression with a void type is effectively an if...else expression; a void-typed conditional expression with an empty second operand is effectively an if expression.

:::java
public static ConditionalExpression condition(
    final Expression test,
    final Expression ifTrue,
    final Expression ifFalse);

public static ConditionalExpression condition(
    final Expression test,
    final Expression ifTrue,
    final Expression ifFalse,
    final Type<?> type);

public static ConditionalExpression ifThen(
    final Expression test,
    final Expression ifTrue);

public static ConditionalExpression ifThenElse(
    final Expression test,
    final Expression ifTrue,
    final Expression ifFalse);

The test expression must have an identity, primitive, or boxing conversion to the boolean type. The types of the ifTrue and ifFalse operands, if both are present, must have a common identity, primitive, or boxing conversion.

Constant Expressions

Constant expressions wrap primitive compile-time constants or complex runtime objects. Note that only compile-time constants can be included in an expression tree that gets compiled to a MethodBuilder. Expressions compiled to callbacks may utilize closures, and may therefore reference complex runtime constants.

:::java
public static ConstantExpression constant(final Object value);
public static ConstantExpression constant(final Object value, final Type type);

Default Value Expressions

A DefaultValueExpression expression evaluates to the default value for the specified type. For all reference types, this value is null. For numeric types, the value will be the type-appropriate zero value. For boolean and char types, the default values will be false and '\0', respectively.

:::java
public static DefaultValueExpression defaultValue(final Type type);

Empty Expressions

The empty() expression is effectively a NOP and is equivalent to defaultValue(void).

:::java
public static Expression empty()

...more to come