Skip to content

Commit cd586ae

Browse files
committed
Specification of DslMarker
1 parent 1d55d2a commit cd586ae

File tree

1 file changed

+317
-0
lines changed

1 file changed

+317
-0
lines changed

notes/005-dsl-marker.md

Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
# DslMarker
2+
3+
* **Type**: Design specification
4+
* **Author**: Alejandro Serrano
5+
* **Contributors**:
6+
7+
## Abstract
8+
9+
This document provides a specification of the behavior of the
10+
[`DslMarker`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-dsl-marker/)
11+
annotation.
12+
13+
## Table of contents
14+
15+
* [Abstract](#abstract)
16+
* [Table of contents](#table-of-contents)
17+
* [Usage](#usage)
18+
* [Marking](#marking)
19+
* [Scope control](#scope-control)
20+
* [Sources](#sources)
21+
22+
## Usage
23+
24+
`DslMarker` is an annotation used for
25+
[scope control](https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker).
26+
In particular, it helps in scenarios in which lots of implicit values are
27+
available, and choosing the wrong one may lead to mistakes. One example
28+
is the [`kotlinx.html`](https://github.com/Kotlin/kotlinx.html) library,
29+
in which nesting in source code represents the nesting structure of a HTML
30+
document. The "current" node is represented as an implicit receivers, so
31+
choosing any other than the innermost leads to ill-formed HTML.
32+
33+
```kotlin
34+
body {
35+
div {
36+
a("https://kotlinlang.org") {
37+
target = ATarget.blank
38+
+"Main site"
39+
}
40+
}
41+
}
42+
```
43+
44+
`DslMarker` is a **meta-annotation**. That means that it is an annotation
45+
which you apply to other annotations, each of them representing a different
46+
**DSL marker**.
47+
48+
```kotlin
49+
@DslMarker annotation class HtmlTagMarker
50+
```
51+
52+
Different DSL markers do not interact with each other; scope control and
53+
propagation is done for each one independently.
54+
55+
## Marking
56+
57+
In order to understand how scope control works with `DslMarker`, we first need
58+
to understand how we decide whether a particular implicit value (implicit
59+
receiver or context parameter) is **marked** with a DSL marker `M`. There are
60+
four potential **sources** of markers.
61+
62+
**Marking through classifier declaration**. If an implicit value has type `T`,
63+
an that type or any of its supertypes is annotated with the DSL marker `M`,
64+
then the implicit value is marked with `M`.
65+
66+
```kotlin
67+
@HtmlTagMarker open class Tag { ... }
68+
open class HTMLTag(...) : Tag()
69+
class HTML(...) : HTMLTag()
70+
class BODY(...) : HTMLTag()
71+
72+
fun HTML.body(block : BODY.() -> Unit = {}) {
73+
// 'this@body' is marked with 'HtmlTagMarker'
74+
// because its supertype 'Tag' is annotated with it
75+
}
76+
77+
fun HTML.userDetails(...) {
78+
// 'this@userDetails' is marked with 'HtmlTagMarker'
79+
// because its supertype 'Tag' is annotated with it
80+
body {
81+
// 'this@body' is marked with 'HtmlTagMarker'
82+
// because its supertype 'Tag' is annotated with it
83+
}
84+
}
85+
```
86+
87+
**Marking through type alias**. If an implicit value has type `T`, and that
88+
type is a type alias annotated with the DSL marker `M`, then the implicit value
89+
is marked with `M`.
90+
91+
```kotlin
92+
@DslMarker annotation class AliasMarker
93+
94+
class A
95+
@AliasMarker typealias B = A
96+
typealias C = B
97+
98+
fun A.usesA() {
99+
// 'this@usesA' is not marked with 'AliasMarker'
100+
}
101+
102+
fun B.usesB() {
103+
// 'this@usesB' is marked with 'AliasMarker'
104+
}
105+
106+
fun C.usesC() {
107+
// 'this@usesC' is marked with 'AliasMarker'
108+
// since 'C' expands to a type alias with the marker
109+
}
110+
```
111+
112+
**Marking through type annotation**. If the type of the implicit value is
113+
annotated with the DSL marker `M`, then the implicit value is marked with `M`.
114+
This annotation may happen everywhere an annotation of a type is allowed,
115+
including (context) parameter declarations or type arguments.
116+
117+
```kotlin
118+
@DslMarker annotation class ExampleMarker
119+
120+
class A
121+
class B
122+
class C
123+
124+
fun foo(block: (@ExampleMarker A).() -> Unit) = ...
125+
126+
fun example1() = foo {
127+
// 'this@example1' is marked with 'ExampleMarker'
128+
// because it's directly annotated
129+
}
130+
131+
context(a: @ExampleMarker A, b: B)
132+
fun (@ExampleMarker C).example2() {
133+
// 'a' is marked with 'ExampleMarker'
134+
// 'b' has no DSL markers
135+
// 'this@example2' is marked with 'ExampleMarker'
136+
}
137+
138+
fun example3() = with<@ExampleMarker A, _>(A()) {
139+
// 'this@with' is marked with 'ExampleMarker'
140+
}
141+
```
142+
143+
**Propagation through function types**. When the annotation is applied to
144+
a function type, we propagate the DSL marker to all the implicit values
145+
(context parameters and receivers).
146+
147+
```kotlin
148+
fun bar(block: @ExampleMarker context(A) B.() -> Unit) = ...
149+
150+
fun example3() = bar {
151+
// context parameter of type 'A' is marked with 'ExampleMarker'
152+
// 'this@bar' is marked with 'ExampleMarker'
153+
// by propagation from the type of 'block'
154+
}
155+
```
156+
157+
Note the potential for confusion between annotating the function type and
158+
the receiver type.
159+
160+
```kotlin
161+
// only receiver is marked
162+
fun quux1(block: context(A) (@ExampleMarker B).() -> Unit) = ...
163+
164+
// context parameter and receiver are marked
165+
fun quux2(block: @ExampleMarker context(A) B.() -> Unit) = ...
166+
// equivalent to
167+
fun quux2(block: context(@ExampleMarker A) (@ExampleMarker B).() -> Unit) = ...
168+
```
169+
170+
In fact, given the _no duplicate markers_ rule described below, introducing
171+
more than one implicit value with the same marker makes the parameter almost
172+
impossible to call.
173+
174+
## Scope control
175+
176+
`DslMarker` only affects **implicit binding**, that is, those scenarios in
177+
which an implicit value is **not** directly specified by the developer, but
178+
rather "chosen" by the compiler. Those scenarios include choosing an
179+
[implicit receiver](https://kotlinlang.org/spec/overload-resolution.html#call-without-an-explicit-receiver)
180+
and [context parameter resolution](https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md#extended-resolution-algorithm).
181+
182+
> [!NOTE]
183+
> Calls with an explicit receiver `e.f(...)` may involve implicit binding
184+
> if the function `f` has both a dispatch and an extension receiver,
185+
> or declares context parameters.
186+
187+
**The general rule**. Whenever an implicit value with DSL marker `M` is bound,
188+
there must not be another implicit value with the same DSL marker in the same
189+
or a closer scope.
190+
191+
We remark the **lazy** nature of the scope control mechanism of `DslMarker`.
192+
If you never perform an operation which requires implicit binding, then **no**
193+
error is raised. In particular,
194+
explicit usages of implicit values are **not** covered by this restriction.
195+
Those explicit usages include
196+
[`this@label` expressions](https://kotlinlang.org/spec/expressions.html#this-expressions),
197+
and access to [context parameters](https://github.com/Kotlin/KEEP/blob/master/proposals/context-parameters.md#declarations-with-context-parameters)
198+
by their name.
199+
200+
From this general rule we can derive two main consequences.
201+
To understand them, we use examples referencing the following declarations:
202+
203+
```kotlin
204+
@DslMarker annotation class Marker
205+
206+
@Marker class A
207+
@Marker class B
208+
/* no @Marker! */ class C
209+
210+
fun A.callAReceiver() { }
211+
fun B.callBReceiver() { }
212+
213+
context(a: A) fun callAContext() { }
214+
context(b: B) fun callBContext() { }
215+
216+
context(a: A, b: B) fun callABContext() {}
217+
```
218+
219+
**No binding to outer scopes**. Consider the innermost scope in which an
220+
implicit value with DSL marker `M` lives. It is **not** allowed to bind
221+
implicit values stemming from outer scopes.
222+
223+
In the example below only access to `B()` is granted; access to `A()` is
224+
restricted because of the presence of `B()`. Note that the additional receiver
225+
of type `C` does not play any role here, since it is not marked with a DSL
226+
marker.
227+
228+
```kotlin
229+
fun example() {
230+
with(A()) {
231+
with(B()) {
232+
with(C()) {
233+
callAReceiver() // error
234+
callBReceiver() // ok
235+
callAContext() // error
236+
callBContext() // ok
237+
callABContext() // error
238+
}
239+
}
240+
}
241+
}
242+
```
243+
244+
The kind of implicit value (context parameter or receiver) does not play
245+
a role in this rule. In the example below access to the receiver of type `A`
246+
is restricted because of a context parameter with the same DSL marker.
247+
248+
```kotlin
249+
fun example() {
250+
with(A()) {
251+
context(B()) {
252+
with(C()) {
253+
callAReceiver() // error
254+
callAContext() // error
255+
}
256+
}
257+
}
258+
}
259+
```
260+
261+
**No duplicate markers on the same scope**. If we implicitly bind a value
262+
with marker `M`, and in the same scope there is another implicit value
263+
marked with the same marker `M`, then we report a DSL violation.
264+
265+
In the examples below we introduce two implicit values with the same DSL
266+
marker in the same scope: in the first one as two context parameters,
267+
in the second one as a context parameter and a receiver. Note how any call
268+
with implicit binding is forbidden, even if only one of the implicit values
269+
are bound. The cases in which we explicitly give a receiver are accepted.
270+
271+
```kotlin
272+
context(a: A, b: B) fun example1() {
273+
callAContext() // error
274+
callBContext() // error
275+
callABContext() // error
276+
}
277+
278+
context(a: A) fun B.example2() {
279+
callAContext() // error
280+
callBReceiver() // error
281+
a.callAReceiver() // ok
282+
this.callBReceiver() // ok
283+
callABContext() // error
284+
}
285+
```
286+
287+
Note that before the introduction of context parameters there was no way to
288+
reach this situation, since implicit receivers are always introduce once at
289+
a time, and are completely ordered as a consequence.
290+
291+
**No further search**. If any of the potential candidates in a given scope
292+
are rejected due to DSL scope violation, the search for candidates does
293+
**not** proceed to outer scopes. In particular, if none of the candidate is
294+
applicable, an error is reported.
295+
296+
This is especially visible in mixed receiver - context parameter scenarios,
297+
since extensions and members take precedence over top-level functions.
298+
In the example below `foo` is resolved to the extension to `A`, which violates
299+
scope control because it binds a value in an outer scope than `B`. However,
300+
search also stops there, so we report an error instead of attempting
301+
resolution with `foo` where `B` is a context parameter.
302+
303+
```kotlin
304+
fun A.foo() { }
305+
context(b: B) fun foo() { }
306+
307+
fun A.bar() = context(B()) {
308+
foo() // error
309+
}
310+
```
311+
312+
## Sources
313+
314+
- Documentation about [`DslMarker`](https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-dsl-marker/)
315+
- Documentation about [type-safe builders](https://kotlinlang.org/docs/type-safe-builders.html#scope-control-dslmarker)
316+
- Source code for `CheckDslScopeViolation` ([snapshot for 2.2.0](https://github.com/JetBrains/kotlin/blob/2.2.0/compiler/fir/resolve/src/org/jetbrains/kotlin/fir/resolve/calls/stages/ResolutionStages.kt#L384))
317+
- Tests for `DslMarker` ([snapshot for 2.2.0](https://github.com/JetBrains/kotlin/tree/2.2.0/compiler/testData/diagnostics/tests/resolve/dslMarker))

0 commit comments

Comments
 (0)