Valhalla breaks minimal-j framework

Ethan McCue ethan at mccue.dev
Tue Dec 2 04:50:38 UTC 2025


There is a parallel thread going on which touches on similar things - there
people are wanting "record component references."

I adapted the code from
https://github.com/rilindbicaj/fluentmapper/blob/main/fluentmapper-provider/src/main/java/dev/bici/fluentmapper/provider/expression/parser/ExpressionParser.java
to use the new classfile api + work for a quick demo of the approach.

Basically, you can get at the code in the body of a lambda via some
serialization hackery. The degree to which this is actually supported is to
me unknown.

import java.io.IOException;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.lang.classfile.ClassFile;
import java.lang.classfile.ClassModel;
import java.lang.classfile.instruction.FieldInstruction;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;

public final class FieldReference {
    private final Class<?> root;
    private final List<Field> fields;

    private FieldReference(Class<?> root, List<Field> fields) {
        this.root = root;
        this.fields = fields;
    }


    public Class<?> root() {
        return root;
    }

    public List<Field> fields() {
        return fields;
    }

    private static SerializedLambda toSerializedLambda(Expression<?, ?>
expression) {
        try {
            var writeReplaceMethod =
expression.getClass().getDeclaredMethod("writeReplace");
            writeReplaceMethod.setAccessible(true);

            return (SerializedLambda) writeReplaceMethod.invoke(expression);
        } catch (ReflectiveOperationException e) {
            throw new IllegalArgumentException("Could not extract
SerializedLambda from the provided expression;", e);
        }
    }

    public static <T> FieldReference of(Class<T> klass, Expression<T, ?>
expression) {
        // TODO: This work can likely be cached.
        var serializedLambda = toSerializedLambda(expression);
        var containingClass = serializedLambda.getImplClass();
        var lambdaSignature = serializedLambda.getImplMethodName();

        // TODO: Unsure if this is the right class loader to use
        var classLoader = klass.getClassLoader();

        byte[] classBytes;
        try (var classResource = classLoader.getResourceAsStream(
                containingClass.replace('.', '/') + ".class"
        )) {
            if (classResource == null) {
                throw new IllegalArgumentException("Could not find
class-file resource in class loader");
            }

            classBytes = classResource.readAllBytes();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        ClassModel classModel = ClassFile.of().parse(classBytes);

        var methodModel = classModel.methods().stream()
                .filter(method ->
lambdaSignature.equals(method.methodName().toString()))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("Could not
find the implementing method for the expression lambda"));
        var code = methodModel.code().orElseThrow();

        // TODO: This could use a lot of prechecks
        var fields = new ArrayList<Field>();

        Class<?> lastType = klass;
        for (var element : code.elementList()) {
            if (element instanceof FieldInstruction fieldInstruction) {
                try {
                    fields.add(

lastType.getDeclaredField(fieldInstruction.field().name().toString())
                    );


                    // TODO: This is almost certainly not sound
                    if
(fieldInstruction.typeSymbol().packageName().isEmpty()) {
                        lastType = classLoader.loadClass(
                                fieldInstruction.typeSymbol().displayName()
                        );
                    }
                    else {
                        lastType = classLoader.loadClass(
                                fieldInstruction.typeSymbol().packageName()
+ "."
                                        +
fieldInstruction.typeSymbol().displayName()
                        );
                    }

                } catch (NoSuchFieldException e) {
                    throw new IllegalArgumentException("Inaccessable
field", e);
                } catch (ClassNotFoundException e) {
                    throw new RuntimeException(e);
                }
            }
        }


        return new FieldReference(klass, List.copyOf(fields));
    }


    @FunctionalInterface
    public interface Expression<A, B> extends Serializable {
        B get(A a);
    }


    @Override
    public String toString() {
        return "FieldReference[" +
                "root=" + root.getName() +
                ", fields=" + fields.stream().map(Field::getName).toList() +
                ']';
    }
}

And you can use all of that with a lambda that only accesses fields to
emulate "field references."

class A {
    String aa;
    B b;
}

class B {
    C c;
}

 class C {
    String v;
    D d;
}

 class D {
    E e;
}

 class E {
    F f;
}

 class F {
    G g;
}

 class G {
    String v;
}

public class Test {
    public static void main(String[] args) {
        var refA = FieldReference.of(A.class, a -> a.aa);
        IO.println(refA);
        var refB = FieldReference.of(A.class, a -> a.b.c.d);
        IO.println(refB);
    }
}

FieldReference[root=A, fields=[aa]]
FieldReference[root=A, fields=[b, c, d]]

So that is one approach to look into.


On Mon, Dec 1, 2025, 8:38 PM Bruno Eberhard <bruno.eberhard at pop.ch> wrote:

> Am 01.12.2025 um 22:00 schrieb Ethan McCue:
> > On that note, i'd say you have somewhat of a bigger problem w.r.t. enums
> > [..]
> > As with all things in jdk.unsupported, I can't help but wonder if there
> > are any VM level invariants you run afoul of by making extra enum
> > instances.
>
> I don't know about the VM internals. I did never run into problems with
> these extra enums.
>
> > My first stab at #2 is to add something like this record [..]
> Thank you for your input. I have to try this.
>
> > I think it's also worth asking - do you have any usage statistics on
> > your library? (Maven central used to offer download stats, etc.) If you
> > are going to be broken anyways it is probably useful to know the blast
> > radius.
>
> Very good point. As far as I know minimal-j is only used by me for a
> smaller and one bigger App. In the bigger App ( An ERP called
> lisheane.ch , currently only available in german ) I invested quite some
> time. I would have to rewrite parts of it.
>
> So the world will not be standing still if minimal-j doesn't work
> anymore or has to be changed.
>
> I would still like the idea of field references ( Person::name ,
> Person::address::city ) in the java language as equivalent for the $
> constant. It is really nice to use. But I don't have the knowledge how
> to add it to the language or how to propose the feature.
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/valhalla-dev/attachments/20251201/617b9788/attachment.htm>


More information about the valhalla-dev mailing list