procyon icon indicating copy to clipboard operation
procyon copied to clipboard

Incorrect byte code generated for nested lambda with closure

Open BladeWise opened this issue 3 years ago • 0 comments

I have encountered an issue compiling a lambda using a closure in a nested lambda.

This is the expression I am trying to generate and compile (the getOrNull method, included below, is basically a null-conditional get accessor): (Ctx ctx) -> getOrNull(ctx, x -> getOrNull(ctx.items.get("key_2"), k -> k.value))

This is the a test to replicate the issue:

public class CustomTests {
    public static <T, R> R getOrNull(final T instance, @NotNull final Func1<T, R> getter) {
        if (instance == null) {
            return null;
        }
        return getter.apply(instance);
    }

    @Test
    public void nestedLambdaWithClosureCompilation() {
        final Type<Ctx> ctxType = Type.of(Ctx.class);
        final Type<Ctx.Item> itemType = Type.of(Ctx.Item.class);
        final MethodInfo getOrNullMethod =
            Type.of(CustomTests.class).getMethod("getOrNull", BindingFlags.PublicStatic, Types.Object, Type.of(Func1.class));

        // Reference expression: (Ctx ctx) -> getOrNull(ctx, x -> getOrNull(ctx.items.get("key_2"), k -> k.value))
        // 1. Create parameter [(Ctx ctx)]
        final ParameterExpression ctxParameter = Expression.parameter(ctxType, "ctx");

        // 2. Create value lambda getter [(Ctx.Item k) -> k.value]
        final ParameterExpression kParameter = Expression.parameter(itemType, "k");
        final MemberExpression itemGetterBody = Expression.field(kParameter, itemType.getField("value"));
        final Type<Func1<Ctx.Item, Object>> itemGetterDelegateType = Type.of(Func1.class).makeGenericType(itemType, Types.Object);
        final LambdaExpression<Func1<Ctx.Item, Object>> itemGetter = Expression.lambda(itemGetterDelegateType, itemGetterBody, kParameter);

        // 3. Create item accessor [ctx.items.get("key_2")]
        final MemberExpression itemsAccessor = Expression.field(ctxParameter, ctxType.getField("items"));
        final MethodCallExpression getValueByKey =
            Expression.call(itemsAccessor, itemsAccessor.getType().getMethod("get", Types.String), Expression.constant("key_2"));

        // 4. Create conditional getter [getOrNull(ctx.items.get("key_2"), k -> k.value)]
        final MethodCallExpression getValueConditional =
            Expression.call(getOrNullMethod.makeGenericMethod(getValueByKey.getType(), Types.Object), getValueByKey, itemGetter);

        // 5. Create conditional getter as lambda getter [(Ctx x) -> getOrNull(ctx.items.get("key_2"), k -> k.value)] (A closure is used instead of the parameter)
        final ParameterExpression xParameter = Expression.parameter(ctxType, "x");
        final Type<Func1<Ctx, Object>> getValueConditionalDelegateType = Type.of(Func1.class).makeGenericType(ctxType, Types.Object);
        final LambdaExpression<Func1<Ctx, Object>> getValueConditionalLambda =
            Expression.lambda(getValueConditionalDelegateType, getValueConditional, xParameter);

        // 6. Create lambda [getOrNull(ctx, x -> getOrNull(ctx.items.get("key_2"), k -> k.value))]
        final MethodCallExpression body = Expression.call(getOrNullMethod.makeGenericMethod(ctxType, Types.Object), ctxParameter, getValueConditionalLambda);
        final LambdaExpression<Func1<Ctx, Object>> lambda =
            Expression.lambda(Type.of(Func1.class).<Func1<Ctx, Object>>makeGenericType(ctxType, Types.Object), body, ctxParameter);

        final Func1<Ctx, Object> delegate = lambda.compile();
        assertNotNull(delegate);
    }

    private static class Ctx {
        public Map<String, Item> items = new LinkedHashMap<>();
        public Double number = 23.3;

        public Ctx() {
            items.put("key_2", new Item());
        }

        public static class Item {
            public Integer value = 55;
        }
    }
}

The compiled byte code fails verification phase (VerifyError). I used ASM to have a better validation error, and analyzed the attached f__Lambda$0x0002.class and it seems that the generated bytecode passes the Closure as a parameter to the actual invoke method, while it should be passed to the constructor of the delegate (if I am not wrong).

BladeWise avatar Mar 09 '22 11:03 BladeWise