Zac Sweers
11/12/2025, 6:38 PMdmitriy.novozhilov
11/12/2025, 8:55 PMZac Sweers
11/12/2025, 9:03 PMZac Sweers
11/12/2025, 9:03 PMdmitriy.novozhilov
11/12/2025, 9:07 PMdmitriy.novozhilov
11/12/2025, 9:07 PMZac Sweers
11/12/2025, 9:08 PMZac Sweers
11/12/2025, 9:16 PMAppModule class is looked up in IrContributionData.findExternalBindingContainerContributions
• It is successfully found and returned by findVisibleContributionClassesForScopeInHints() , which looks up hints in a "known package" and derives contributing classes from this set
• Then it passes those on to getScopedContributions and loses the class in filtering on this block
contributions.filter { irClass -> with(metroContext) { irClass.isBindingContainer() } }
(line 116 in main)
This filter fails because isBindingContainer() looks for a @BindingContainer annotation, and that's where I saw that the class is missing all annotations and member declarations.
The class itself is defined in the test in appModuleContent and should look like so
@BindingContainer
@ContributesTo(Unit::class)
interface AppModule {
@Binds
@IntoSet
fun bindMultibinding(multibinding: AppMultibinding): Multibinding
}Zac Sweers
11/12/2025, 9:17 PMZac Sweers
11/12/2025, 9:17 PMZac Sweers
11/12/2025, 9:18 PMZac Sweers
11/12/2025, 9:20 PMIrMetroContext.writeDiagnostic API since I'm not able to attach a debugger to the kotlinc process itself. If you breakpoint the gradle IC test though on the third compilation result, you can poke around that output directory (if you write anything) via project.asMetroProject.appGraphReports.reportsDir in the debuggerdmitriy.novozhilov
11/13/2025, 9:38 AMval debugArg = """-Dkotlin.daemon.jvm.options="-agentlib:jdwp=transport=dt_socket\,server=n\,suspend=y\,address=5005""""
val buildResult = build(project.rootDir, "compileKotlin", debugArg)
2. Create Remote JVM debug run configuration and set it to Listen to remote JVM mode (I also recommend enabling Auto restart option)
3. Run this run configuration before tests
4. Run test (with or without debugger, doesn't matter)dmitriy.novozhilov
11/13/2025, 9:58 AMfindExternalBindingContainerContributions and getScopedContributions are not called on the third compilation.
I don't have enough context, to understand where to move next, sorry.
But I believe that you pick it up from here with these instructions.
I'll be waiting for more inputs from you.Zac Sweers
11/13/2025, 3:00 PMZac Sweers
11/13/2025, 6:19 PMZac Sweers
11/13/2025, 6:21 PMZac Sweers
11/13/2025, 6:26 PMZac Sweers
11/13/2025, 6:27 PMZac Sweers
11/13/2025, 6:27 PMZac Sweers
11/13/2025, 7:35 PMSUCCESS but I can't find any evidence that the compiler is actually running vs just possibly restoring from cache (but with the second compilation's cache result and not the first)? It's hard to say 😕Zac Sweers
11/13/2025, 8:33 PMZac Sweers
11/13/2025, 8:35 PMdmitriy.novozhilov
11/13/2025, 9:14 PMZac Sweers
11/13/2025, 9:14 PMZac Sweers
11/13/2025, 9:15 PMZac Sweers
11/13/2025, 10:32 PMICReproTransformer transformer that just swaps a function impl with some text based on whether a class it looks up from pluginContext is present or not (and links their compilations for IC). Test I think basically confirms my theory above toodmitriy.novozhilov
11/14/2025, 12:11 PMICReproTransformer you lookup for Marker class regardless of any conditions, but write lookup only if it was actually found.
So if you replace
if (markerClass != null) {
trackClassLookup(collectorClass, markerClass)
}
with
trackClassLookup(collectorClass, markerClassId)
where there is newly introduced
context(context: IrMetroContext)
internal fun trackClassLookup(callingDeclaration: IrDeclaration, calleeClassId: ClassId) {
callingDeclaration.withAnalyzableKtFile { filePath ->
val container = calleeClassId.outerClassId?.asSingleFqName() ?: calleeClassId.packageFqName
trackLookup(
container = container,
declarationName = calleeClassId.shortClassName.asString(),
scopeKind = ScopeKind.PACKAGE,
location =
object : LocationInfo {
override val filePath = filePath
override val position: Position = Position.NO_POSITION
},
)
}
}
the test will start passing.dmitriy.novozhilov
11/14/2025, 12:13 PMclass A
fun A.foo() {}
fun test(a: A) {
a.foo()
}
Here we resolve to function ext A.foo, but write lookups that we searched for member A.foo and ext A.foo.
So if class A changes and the member fun foo() is added, the file with test functions becomes dirty, even if the extension function was unchanged.Zac Sweers
11/17/2025, 5:22 AMZac Sweers
11/17/2025, 5:24 AMdmitriy.novozhilov
11/17/2025, 6:55 AMbut that's only possible if you know the classid ahead of timeHow do you get to know class id in the real case? Using lookup predicate?
dmitriy.novozhilov
11/17/2025, 12:00 PMZac Sweers
11/17/2025, 3:59 PMZac Sweers
11/17/2025, 11:35 PMmetro.hints package, in the shape of fun AppScope(source: SourceClass)
• In IC, we were seeing that SourceClass was being marked as dirty when it was restored (correct) but its generated hint was not, and thus was also being kept from the main compilation after it
• main compilation looks for that hint to then look up SourceClass
• Fix was to track a lookup from the generated hint function to the SourceClass parameter so they'd get marked as dirty together