Valhalla breaks minimal-j framework
Ethan McCue
ethan at mccue.dev
Tue Dec 2 15:35:44 UTC 2025
var form = new Form<Person>();
form.line(() -> $.number);
form.line(() -> $.name);
form.line(() -> $.address.city);
Backend.find(Person.class, By.field(() -> $.name, "Bruno"));
Backend.find(Person.class, By.field(() -> $.address.zip, 8000));
This might be all that is needed - forgiving the perf difference, general
hackiness, etc.
On Mon, Dec 1, 2025 at 11:50 PM Ethan McCue <ethan at mccue.dev> wrote:
> 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/20251202/fd5c1417/attachment-0001.htm>
More information about the valhalla-dev
mailing list