-
Notifications
You must be signed in to change notification settings - Fork 118
Expressions Framework
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.
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.
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 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);
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 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 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);
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);
The empty()
expression is effectively a NOP
and is equivalent to defaultValue(void)
.
:::java
public static Expression empty()
...more to come