"Must be initialized" errors not detected when used in inlined lambdas

Consider this code:

class A {
       val x = y
       val y = 7
}

It’s clear that this will fail to compile because y is accessed too early:

Variable 'y' must be initialized

However, if we add a lambda:

class A {
       val x = run { y }
       val y = 7
}

then the code compiles fine, and the value assigned to x is zero. If I use val y = "a", then the result is instead null, even if I declare x as a non-null String. This all occurs silently, with no indication that there’s an ordering issue aside from the mysteriously incorrect value.

This would be understandable if I had used a normal lambda, but here I used an inline (and specifically not crossinline) lambda, so the compiler should be able to realize that the access of y to evaluate x will take place before y is assigned. Yet it doesn’t, allowing what should be a quick fix compilation error to bleed into the runtime.

2 Likes

With code

class A {
    val x = run { y }
    val y = 1
}

I decompile the class file with CFR and got the following java source:

Source: A.java
/*
 * Decompiled with CFR 0.152.
 * 
 * Could not load the following classes:
 *  kotlin.Metadata
 */
import kotlin.Metadata;

@Metadata(mv={1, 6, 0}, k=1, xi=48, d1={"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002R\u0011\u0010\u0003\u001a\u00020\u0004\u00a2\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006R\u0014\u0010\u0007\u001a\u00020\u0004X\u0086D\u00a2\u0006\b\n\u0000\u001a\u0004\b\b\u0010\u0006"}, d2={"LA;", "", "()V", "x", "", "getX", "()I", "y", "getY"})
public final class A {
    private final int x;
    private final int y;

    public A() {
        A $this$x_u24lambda_u2d0 = this;
        A a = this;
        boolean bl = false;
        int n = $this$x_u24lambda_u2d0.getY();
        a.x = n;
        this.y = 1;
    }

    public final int getX() {
        return this.x;
    }

    public final int getY() {
        return this.y;
    }
}

It seems like the final field x is set to this.y before the field x being
set to the wanted value 1. JVM will give all uninitialized field, in this example
it is y, with a default value. And the default value for type int is 0.
Plus, the default value for an object is null, this make your String set to null.

And compare to source code:

class B {
    val x = 1
    val y = run { x }
}

and the decompile output:

Source: B.java
/*
 * Decompiled with CFR 0.152.
 * 
 * Could not load the following classes:
 *  kotlin.Metadata
 */
import kotlin.Metadata;

@Metadata(mv={1, 6, 0}, k=1, xi=48, d1={"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002R\u0014\u0010\u0003\u001a\u00020\u0004X\u0086D\u00a2\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006R\u0011\u0010\u0007\u001a\u00020\u0004\u00a2\u0006\b\n\u0000\u001a\u0004\b\b\u0010\u0006"}, d2={"LB;", "", "()V", "x", "", "getX", "()I", "y", "getY"})
public final class B {
    private final int x;
    private final int y;

    public B() {
        this.x = 1;
        B $this$y_u24lambda_u2d0 = this;
        B b = this;
        boolean bl = false;
        int n = $this$y_u24lambda_u2d0.getX();
        b.y = n;
    }

    public final int getX() {
        return this.x;
    }

    public final int getY() {
        return this.y;
    }
}

I see that the statement val y = run { x } was compiled to

B $this$y_u24lambda_u2d0 = this;
B b = this;
boolean bl = false;
int n = $this$y_u24lambda_u2d0.getX();
b.y = n;

I also create two inline functions:

  • inline fun <T, R> T.func1(a: T.() -> R): R = a(this)
  • inline fun <T, R> T.func2(a: R): R = a

With func1 in C.kt

class C {
    val x = func1 { y }
    val y = 1
}

We get decompile output

Source: C.kt
/*
 * Decompiled with CFR 0.152.
 * 
 * Could not load the following classes:
 *  kotlin.Metadata
 */
import kotlin.Metadata;

@Metadata(mv={1, 6, 0}, k=1, xi=48, d1={"\u0000\u0014\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\b\n\u0002\b\u0004\u0018\u00002\u00020\u0001B\u0005\u00a2\u0006\u0002\u0010\u0002R\u0011\u0010\u0003\u001a\u00020\u0004\u00a2\u0006\b\n\u0000\u001a\u0004\b\u0005\u0010\u0006R\u0014\u0010\u0007\u001a\u00020\u0004X\u0086D\u00a2\u0006\b\n\u0000\u001a\u0004\b\b\u0010\u0006"}, d2={"LC;", "", "()V", "x", "", "getX", "()I", "y", "getY"})
public final class C {
    private final int x;
    private final int y;

    /*
     * WARNING - void declaration
     */
    public C() {
        void $this$x_u24lambda_u2d0;
        int n;
        C $this$func1$iv = this;
        boolean $i$f$func1 = false;
        C c = $this$func1$iv;
        C c2 = this;
        boolean bl = false;
        c2.x = n = $this$x_u24lambda_u2d0.getY();
        this.y = 1;
    }

    public final int getX() {
        return this.x;
    }

    public final int getY() {
        return this.y;
    }
}

And with func2 in D.kt

class D {
    val x = func2(y)
    val y = 1
}

Now we get the wanted compile error

D.kt:2:19: error: variable 'y' must be initialized
    val x = func2(y)

Note that run accept a function as argument, this make the difference.

But why?

The inline function run does not call the function at compile time, but insert the argument function call into the generated byte code. So the result is the function passed to run is called when the class constructs, when the final field x is not initialized but JVM has given it a default value.

For func1, we can pass a function that returns y to x, kotlin will call the function directly in the constructor.
For func2, it is clear that we cannot get y as argument because it is not initialized yet.

Build a function gets y is possible, but get y as an argument is impossible.

I believe OP already know everything you said. This is not what they asked.

OP has a good point. run() is fully inlined, so the compiler could report this as an error. I guess this is just an improvement that was not (yet) done.

1 Like

Yes, I understand that the issue is that y is accessed in the lambda. Had the compiler done the further step of inlining the getY() method call with y, which should have been easily possible for a final method like that, it would realize the problem.