How does the scope restriction differ when two nested receivers share the same @DslMarker annotation versus carrying different marker annotations? When would you deliberately use distinct markers?
answer
- Rule is per-marker, computed independently
- Same marker → isolation (outer hidden)
- Different markers → both implicitly visible
- Distinct markers = intentional interoperation
- Accidental distinct markers = silent leak
basics
~20 sShadowing only happens between receivers with the SAME marker. Different markers don't hide each other, so the outer receiver stays implicitly callable. Use distinct markers when you intentionally want an inner DSL to reach an outer, unrelated one.
solid answer
~40 sThe compiler's rule is per-marker: among implicit receivers annotated with the *same* @DslMarker annotation, only the nearest is unqualified-accessible. Receivers carrying *different* markers — or one marked and one unmarked — do not shadow each other; both remain implicitly available. So within a single cohesive DSL you use one shared marker to prevent any cross-level leakage. You deliberately use distinct markers when nesting two independent DSLs and you *want* the inner block to call the outer DSL's members implicitly (e.g. an HTML DSL embedded inside a routing/config DSL where you intend to call config helpers from inside HTML). It's a design lever: same marker = isolation; different markers = intentional interoperation. Misuse — accidentally giving sibling builders different markers — silently reintroduces the very leakage @DslMarker exists to stop.
code
kotlin · 13 lines@DslMarker annotation class ConfigDsl
@DslMarker annotation class HtmlDsl
@ConfigDsl class ConfigBuilder { fun setting(k: String) {} ; fun html(b: HtmlBuilder.() -> Unit) {} }
@HtmlDsl class HtmlBuilder { fun p(t: String) {} }
fun config(b: ConfigBuilder.() -> Unit) = ConfigBuilder().apply(b)
fun demo() = config {
html {
p("hi") // nearest receiver
setting("env") // OK: different marker, outer stays visible
}
}go deeper
Likely only knows the same-marker isolation case, not the cross-marker behavior.
Understands same-marker shadowing but may not articulate that different markers stay visible.
Clearly explains the per-marker rule and gives a deliberate use case for distinct markers.
Treats marker choice as part of the public DSL contract, weighs interoperability vs. isolation, and anticipates silent-leak regressions from inconsistent markers.
## The rule restated precisely @DslMarker partitions implicit receivers by their marker annotation. For each marker annotation `M`: - Among all in-scope implicit receivers annotated with `M`, **only the innermost (nearest)** is accessible for unqualified member access. - Outer receivers annotated with `M` are hidden (still reachable via `this@label`). Crucially, this is computed **independently per marker**. Two receivers with *different* markers are in different partitions and never shadow one another. ## Same marker → isolation ```kotlin @DslMarker annotation class HtmlDsl @HtmlDsl class HtmlBuilder { fun meta() {} ; fun body(b: BodyBuilder.() -> Unit) {} } @HtmlDsl class BodyBuilder html { body { /* meta() -> ERROR, HtmlBuilder hidden */ } } ``` Both carry `HtmlDsl`, so the inner block hides the outer — the safe default. ## Different markers → interoperation ```kotlin @DslMarker annotation class ConfigDsl @DslMarker annotation class HtmlDsl @ConfigDsl class ConfigBuilder { fun setting(k: String) {} ; fun html(b: HtmlBuilder.() -> Unit) {} } @HtmlDsl class HtmlBuilder { fun p(text: String) {} } config { html { p("hi") // HtmlBuilder, nearest setting("x") // ConfigBuilder still implicitly visible — different marker! } } ``` Here `setting("x")` from the outer `ConfigBuilder` is callable inside the inner HTML block *because the markers differ*. This is sometimes exactly what you want. ## When to choose distinct markers deliberately - **Embedding one DSL in another** where inner blocks legitimately need the outer DSL's helpers (e.g. Ktor-style routing config nesting an HTML response DSL, where you still want config-level functions available). - **Layered DSLs** with a clear 'ambient context' that should remain reachable. ## The danger of accidental distinct markers If two sibling builders that *should* be isolated end up with different markers (e.g. someone forgot to reuse the shared marker, or copy-pasted a new one), the shadowing silently disappears and accidental cross-scope calls compile again — a subtle regression with no error to flag it. ## Unmarked receivers A receiver with *no* marker behaves like 'different marker' relative to everything: it never shadows and is never shadowed by marker-based rules. So forgetting to mark one level leaves that level leaky. ## Design guidance - Default: **one marker per cohesive DSL**, applied to every receiver type. - Multiple markers: only when cross-scope implicit access is a **deliberate feature**. - Document the marker choice; it's effectively part of the DSL's API contract.
- If a nested receiver is unmarked, how does it interact with a marked outer receiver?An unmarked receiver neither shadows nor is shadowed by marker rules, so the marked outer receiver remains implicitly visible — effectively no protection at that level.
- Give a concrete case for wanting different markers.Embedding an HTML-response DSL inside a server-config/routing DSL where you still want to call config-level helpers from within the HTML block; distinct markers keep both receivers implicitly available.
saying these in an interview costs you the question
- Claiming @DslMarker hides outer receivers regardless of which marker they carry
- Not realizing different markers leave both receivers visible
- Treating multiple markers as always a mistake
- Forgetting that unmarked receivers are never protected
- Saying the rule looks at all receivers together rather than per-marker