Explain multi-round processing in KSP: why does process() run multiple rounds, what does returning deferred symbols mean, and how does this compare to kapt's rounds?
answer
- Generated code can need more processing => rounds
- process() returns List<KSAnnotated> = deferred
- validate() gates ready vs deferred
- rounds until no new files + nothing deferred
- kapt = javac RoundEnvironment, no explicit return
basics
~20 sSome generated code creates new things other processors must then look at. So processing runs in repeated rounds until nothing new appears. In KSP, a processor returns the symbols it couldn't fully handle yet, and they're retried in the next round.
solid answer
~40 sAnnotation processing is **multi-round** because generated code can itself contain symbols that need processing, or a symbol may reference a type that doesn't exist yet because another processor will generate it. In KSP, `SymbolProcessor.process(resolver)` returns a `List<KSAnnotated>` of **deferred symbols** — those it could not fully resolve this round (e.g. they depend on yet-to-be-generated types). KSP re-invokes `process()` in subsequent rounds, re-feeding the deferred symbols, until a round produces no new files and no deferrals. `finish()` is then called for cleanup. This mirrors kapt/javac's `RoundEnvironment` model where processors run repeatedly over rounds until no new sources are generated. The key KSP-specific mechanic is **explicitly returning unresolved symbols** rather than relying solely on the framework re-scanning; correct deferral handling prevents 'symbol not found' errors when generation order matters.
go deeper
Aware that processing can run more than once when new code is generated, without the deferral mechanics.
Explains rounds exist because generated code needs reprocessing and that KSP retries until stable.
Describes returning deferred KSAnnotated from process(), validate() gating, finish(), and maps it to kapt's RoundEnvironment loop.
Reasons about generation-order dependencies across processors, idempotency/duplicate-output risks, and designing processors that defer correctly at scale.
## Why processing needs multiple rounds A single pass isn't enough because **generated code can trigger more processing**: - A processor emits a new class annotated with something another processor handles. - A symbol you're processing **references a type that hasn't been generated yet** (generation order). You can't resolve it this round, so you must wait. So the framework runs in **rounds**: process → generate → re-scan/re-process → … until a round generates nothing new. ## KSP's round model and deferred symbols Your processor implements: ```kotlin class MyProcessor( private val codeGenerator: CodeGenerator, private val logger: KSPLogger ) : SymbolProcessor { override fun process(resolver: Resolver): List<KSAnnotated> { val symbols = resolver .getSymbolsWithAnnotation("com.example.MyAnno") .toList() val (ready, deferred) = symbols.partition { it.validate() } ready.filterIsInstance<KSClassDeclaration>().forEach { decl -> // generate code for fully-resolvable symbols } // hand back what we couldn't process yet return deferred } override fun finish() { /* optional cleanup */ } } ``` Key points: - **`process()` returns `List<KSAnnotated>`** — the **deferred** symbols you could not handle this round (commonly the ones where `validate()` is false because a referenced type isn't generated yet). - KSP **re-runs `process()`** in the next round, and the previously deferred symbols become resolvable once the missing types exist. - `validate()` is the idiomatic check: it returns true only when a symbol and the types it depends on are fully resolved. - Rounds continue until **no new files are generated and nothing is deferred**; then **`finish()`** runs once. - Each round, `getSymbolsWithAnnotation(...)` returns symbols **newly visible** that round (including those in code generated by the previous round), plus your returned deferrals are retried. ## Comparison to kapt / javac rounds kapt delegates to **javac's** `javax.annotation.processing` model: - Processors implement `process(annotations, roundEnv)` and inspect `RoundEnvironment`. - javac loops: each round, newly generated sources are compiled to elements and processors run again, until a round generates no new sources; then a final round runs with `roundEnv.processingOver() == true`. - There is **no explicit 'return deferred symbols' return value**; the framework re-scans the new sources automatically, and processors track their own pending state. KSP makes deferral **explicit and first-class** (the return value), which gives precise control over generation-order dependencies and is the idiomatic way to avoid premature 'unresolved type' failures. ## Common pitfalls - **Not deferring** symbols that reference not-yet-generated types → spurious resolution failures. - **Re-processing already-handled symbols** every round → duplicate output; only defer what's genuinely unresolved. - **Doing work in the wrong place** → use `finish()` for end-of-run aggregation, not per-round side effects that assume completeness. ## Summary Multi-round exists because generated code feeds further generation. KSP models this by having `process()` **return deferred `KSAnnotated` symbols** that are retried each round until quiescence, then `finish()`. kapt achieves the same outcome through javac's `RoundEnvironment` loop, but without an explicit deferred-return value.
- What does returning a non-empty list from process() cause KSP to do?It marks those symbols as deferred; KSP re-invokes process() in a later round and re-supplies them, so they get another chance once dependencies are generated.
- When is finish() called and what's it for?Once, after all rounds complete and nothing more is deferred or generated. It's for final aggregation/cleanup that needs the whole run to be done.
It's like grading essays where some cite a classmate's not-yet-written essay: you set those aside and re-grade them in a later pass once the references exist.
saying these in an interview costs you the question
- Claiming KSP processing is always a single pass
- Saying deferred symbols are silently dropped
- Confusing finish() with a per-round callback
- Deferring everything or nothing regardless of validate()
- Not knowing kapt rounds map to javac RoundEnvironment