Just updated kotlin in my project and it seems I n...
# compiler
m
Just updated kotlin in my project and it seems I now have to use context parameters instead of context receivers, what's the syntax to add it to the scope? I tried
context(_: MyType)
but the methods on it don't seem to resolve.
y
> The methods on it don't seem to resolve > That's the key difference between context receivers and context parameters!. There was a problem with scope pollution: i.e. types would bring in a ton of methods that you didn't really want. So now, only functions that take a type as a context will resolve. Thus, you need "bridge functions": functions that take a context and end up calling your desired method on that context. I recommend this great blog post by Alejandro from the Kotlin Language Design team for the mental model he recommends for context parameters
m
I see, so I need to make a
context(ctx: MyType) fun thing() = ctx.thing()
for each method on MyType now, and add imports for those methods everywhere?
👌 1
y
Precisely. I keep debating whether I should make a KSP plugin to automate it or not lol
m
And is there a way to have intelligent autocomplete for this like with context receivers? Or is that just a lost cause until you import the individual methods?
p
@Youssef Shoaib [MOD] I'm still looking forward to Wither allowing you to do something like
Copy code
fun doSomething {
  with(contextOf<MyType>())
  thing()
}
y
I think autocomplete works just fine, but I can't remember. It should be basically the same as importing top-level functions
p
you could do something like
context(ctx: MyType) inline fun <A> typeScope(f: MyType.() -> A): A = ctx.f()
and then use
typeScope { ... }
to lift the context into a received within a given scope
m
Unfortunately it seems I don't get autocomplete on methods available in the scope unless they're already imported in the file.
y
@phldavies I think this just works:
Copy code
context(_: MyType)
fun doSomething() {
  with(contextOf<MyType>())
  thing()
}
but I imagine you probably want something like:
Copy code
context(@With _: MyType)
fun doSomething() {
 thing()
}
which is doable, but with hacky workarounds
p
ah yes, I remember - it's been a while 😄
m
you could do something like ...
Unfortunately due to the massive scope of this project it's probably not an option, I might just pin this specific submodule on a different kotlin version because context receivers were much easier to maintain this codebase with.
y
with this function in a different package:
Copy code
context(_: Unit)
fun unit() {}
I was able to get auto complete just typing
un
without importing it, and choosing the autocomplete adds the import Just a shot in the dark: maybe your IDE is out of date?
How many such
MyType
types do you have? Is the problem adding the bridge methods, or importing them? I understand your frustration here, but hopefully there's some way to get the migration working (and then you never have to worry about it)
m
The bridge methods are fine, just a major chore for the 50 or so methods on the various scope types but that's manageable. The issue is that IDEA (EAP) doesn't seem to show them in autocomplete which will have a serious impact on implementing new features.
y
That's really, really strange. I'm on EAP too, and I'm getting autocomplete even from other modules. Maybe file a bug?
m
Looks like there's another difference too; I had a
Scope.error(message)
method, but that doesn't seem to resolve correctly, instead resolving to kotlin.error
y
That's extra weird. It should either resolve to the
Scope
version (once you make a bridge version obviously), or it should error (because it's an ambigious overload)
m
I think it's because the function isn't imported so it defaults to kotlin.error, and using tab to import also fails for the same reason?
y
I'm surprised that using tab to import doesn't work. It works for me, even showing up in autocomplete.
kotlin.error
is the first option to show up, expectedly, but then
context(_: Unit) fun error
shows up, and it imports when picked Edit: it seems I'm actually not on a latest EAP, so maybe it's a regression. I'll switch to the newest and see what happens Edit 2: It still works the same on latest EAP. Here's my IDEA info:
Copy code
IntelliJ IDEA 2026.2 EAP
Build #IU-262.7132.23, built on June 4, 2026
Source revision: 88faa234249d6
Runtime version: 25.0.3+9-b496.62 aarch64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
JCEF version: 144.0.15-262-b37
Toolkit: sun.lwawt.macosx.LWCToolkit
macOS 26.5
Kotlin analyzer version: 2.4.20-dev-4439
Registry:
  debugger.new.tool.window.layout=true
  ide.experimental.ui=true
  cache.folding.model.on.disk=false
  kotlin.k2.only.bundled.compiler.plugins.enabled=false
  freeze.reporter.profiling=false
  kotlin.scripting.index.dependencies.sources=true
Kotlin: 262.7132.23-IJ
a
fwiw, IntelliJ has a way to automatically migrate all context receivers into context parameters, without the need of introducing all those bridge methods
I would strongly discourage to keep thinking in the "context receivers fashion" and embrace the way context parameters are supposed to be used
m
embrace the way context parameters are supposed to be used
Unfortunately that's not an acceptable use case here, the only alternative would be to wait for intersection types and to use one of those as receiver somehow without using extension methods
a
the only alternative would be to wait for intersection types
why? you can have multiple context arguments, but have them resolve to the same value. If you have a more specific example that you could share, that would be great; but in my investigation most cases that seemed to require intersections could be handled otherwise
m
In my case I'm working on various interpreters, and to handle control flow and manage coroutines (in cases like Lua), there's a
LuaCoroutineScope
context receiver with methods like continue, break, goto, but also references to upvalues, scope, etc. This keeps code clean since it can just be something like
Copy code
context(LuaCoroutineScope)
suspend fun TValue<*>.luaToString() {
    val meta = this.metatable
    if (meta is TTable) {
        val tostringMeta = meta["__tostring"]
        if (tostringMeta !is TNil) {
            val out = collectAsLuaScope {
                tostringMeta.luaCall(listOf(this@luaToString))
            }.first()
            if (out !is TString) {
                error_("'__tostring' must return a string")
            }
            return_ (out)
        }
    }

    when (this) {
        is TNil -> return_ (TString("nil"))
        is TBoolean -> return_ (TString(if (value) "true" else "false"))
        is TNumber<*> -> return_ (TString(value.toString()))
        is TString -> return_ (this)
        is TFunction -> return_ (TString("function: 0x${this.hashCode().toString(16)}"))
        else -> {
            var name = when (this) {
                is TUserdata -> "userdata"
                is TTable -> "table"
                else -> "unknown"
            }

            if (meta is TTable) {
                val nameMeta = meta["__name"]
                if (nameMeta is TString) {
                    name = nameMeta.value
                }
            }

            return_ (TString("$name: 0x${hashCode().toString(16)}"))
        }
    }
}
instead of having to do something like
Copy code
context(ctx: LuaCoroutineScope)
suspend fun TValue<*>.luaToString() {
    val meta = this.metatable
    if (meta is TTable) {
        val tostringMeta = meta["__tostring"]
        if (tostringMeta !is TNil) {
            val out = ctx.collectAsLuaScope {
                tostringMeta.luaCall(listOf(this@luaToString))
            }.first()
            if (out !is TString) {
                ctx.error_("'__tostring' must return a string")
            }
            ctx.return_(out)
        }
    }

    when (this) {
        is TNil -> ctx.return_(TString("nil"))
        is TBoolean -> ctx.return_(TString(if (value) "true" else "false"))
        is TNumber<*> -> ctx.return_(TString(value.toString()))
        is TString -> ctx.return_(this)
        is TFunction -> ctx.return_(TString("function: 0x${this.hashCode().toString(16)}"))
        else -> {
            var name = when (this) {
                is TUserdata -> "userdata"
                is TTable -> "table"
                else -> "unknown"
            }

            if (meta is TTable) {
                val nameMeta = meta["__name"]
                if (nameMeta is TString) {
                    name = nameMeta.value
                }
            }

            return_(TString("$name: 0x${this.hashCode().toString(16)}"))
        }
    }
}
Though this is one of the simpler examples so it may not seem like much effort, but with hundreds of functions it becomes very verbose and things like
ctx.return_(...)
signal control flow worse than a
return_ (...)