I believe I found another bug with context receive...
# getting-started
r
I believe I found another bug with context receivers, but before I submit a new bug report, I'd like to do a quick sanity check here to make sure I'm not just making a dumb mistake. Code in thread.
👀 1
Given the following:
Copy code
interface Gen<A : Gen.A, B : Gen.B> {
    fun a(): A
    fun b(): B

    open class A
    open class B
}

class Values

class Scope<A : Gen.A, B : Gen.B>(
    val gen: Gen<A, B>
) {
    operator fun String.invoke(values: context(Values) B.() -> Unit) {
        with(Values()) {
            values(this, gen.b())
        }
    }
}

operator fun <A : Gen.A, B : Gen.B> Gen<A, B>.invoke(
    rules: context(Scope<A, B>) A.() -> Unit
) {
    with(Scope(this)) {
        rules(this, a())
    }
}
This does not compile:
Copy code
object MyGen : Gen<MyGen.A, MyGen.B> {
    override fun a() = A()
    override fun b() = B()

    open class A : Gen.A() {
        val a = "a"
    }

    open class B : Gen.B() {
        val b = "b"
    }
}

fun main() {
    MyGen {
        a {
            // `this` is `Any?`, but should be `MyGen.B`
            println(b)  // Error: Unresolved reference: b
        }
    }
}
Is my assumption flawed that
this
should be
MyGen.B
where I marked it?
The tooltip when for the invoke call on
a
shows
Copy code
public final operator fun String.invoke(
    values: @ContextFunctionTypeParams(count = 1)() (P.() -> Unit)
): Unit
Which may indicate the problem, but I don't know.
y
so I made a little function like this to see what type parameters the
Scope
has:
Copy code
fun <S: Gen.A, P: Gen.B> Scope<S, P>.scopeId() = this
(this is really useful when debugging weird context receivers issues since you can't quite see a list of context receivers rn) and hitting Ctrl-Shift-P in IDEA says that the
Scope
is of type
Scope<MyGen.A, out Any?>
which is really weird
Adding a bit of TypeWrapper magic (same workaround I gave for the original bug) fixes the issue, which leads me to believe that it might be the same bug:
Copy code
operator fun <A : Gen.A, B : Gen.B> Gen<A, B>.invoke(
    rules: context(Scope<A, B>) A.(TypeWrapper<B>) -> Unit
) {
    with(Scope(this)) {
        rules(this, a(), TypeWrapper.IMPL)
    }
}
fun main() {
    MyGen {
        a {
            scopeId()
            // `this` now shows `MyGen.B`
            println(b)  // Error: Unresolved reference: b
        }
    }
}

fun <S: Gen.A, P: Gen.B> Scope<S, P>.scopeId() = this
r
Ah, good catch. I guess I should just add this to that bug then.
y
This looks like the compiler is again removing any type parameters that don't appear in the main signature of the lambda and only appear in the context bit, resulting in that lambda having no idea that those parameters exist. I'd say yeah absolutely add that to the bug
👍 1
Hopefully this'll be fixed soon in a future context receiver prototype. Meanwhile use the TypeWrapper workaround or any other way of mentioning the very last type parameter context inside of the main lambda signature (e.g. you can use a List just the same as a type wrapper, passing in an empty list, I personally just use type wrappers as my personal pattern-of-choice cuz it carries practically no overhead (just as much overhead as Units really)) It is very interesting though to know that this is just a type parameter issue. Like, I'm speculating but, it might be quite a short change in the compiler code to just consider those type parameters to be "real" type parameters and it can fix that one significant bug.
r
Indeed, and it does appear to be a rather significant bug (at least for my use cases). It seems almost every issue I run into with context receivers is eventually traced back to this.
I like
TypeWrapper
for a workaround as well. Not just for the overhead, but it also is much more semantically clear what the parameter is there for (and I can throw on a deprecation annotation for good measure).
y
A bit off-topic, but TypeWrappers can be useful in other ways too. I originally came up with that pattern when I wanted to emulate partial type parameter inference (i.e. only specifying some type parameters when calling a function with many type parameters). For instance, I used them here to prevent having a caller specify an intersection type as a type argument (which is currently impossible). They can be used in general whenever you expect that users will probably specify some specific type parameters but might leave the others up for grabs (you can use multiple TypeWrappers for different possible sets of specifiable types, and just ensure you have default values of
type()
). In the same thread, I provided this list of TypeWrappers up to 22 type params.