-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathhow-dynamicmemberlookup-works-internally-in-swift.html
More file actions
468 lines (416 loc) · 31.9 KB
/
how-dynamicmemberlookup-works-internally-in-swift.html
File metadata and controls
468 lines (416 loc) · 31.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://use.fontawesome.com/afd448ce82.js"></script>
<!-- Meta Tag -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<!-- SEO -->
<meta name="author" content="Bruno Rocha">
<meta name="keywords" content="Software, Engineering, Blog, Posts, iOS, Xcode, Swift, Articles, Tutorials, OBJ-C, Objective-C, Apple">
<meta name="description" content="The @dynamicMemberLookup attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python.">
<meta name="title" content="How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)">
<meta name="url" content="https://swiftrocks.com/how-dynamicmemberlookup-works-internally-in-swift">
<meta name="image" content="https://swiftrocks.com/images/thumbs/thumb_dark.jpg">
<meta name="copyright" content="Bruno Rocha">
<meta name="robots" content="index,follow">
<meta property="og:title" content="How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)"/>
<meta property="og:image" content="https://swiftrocks.com/images/thumbs/thumb_dark.jpg"/>
<meta property="og:description" content="The @dynamicMemberLookup attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python."/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://swiftrocks.com/how-dynamicmemberlookup-works-internally-in-swift"/>
<meta name="twitter:card" content="summary_large_image"/>
<meta name="twitter:image" content="https://swiftrocks.com/images/thumbs/thumb_dark.jpg"/>
<meta name="twitter:image:alt" content="Page Thumbnail"/>
<meta name="twitter:title" content="How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)"/>
<meta name="twitter:description" content="The @dynamicMemberLookup attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python."/>
<meta name="twitter:site" content="@rockbruno_"/>
<meta name="fediverse:creator" content="@rockbruno@hachyderm.io">
<!-- Favicon -->
<!-- 16x16 -->
<link rel="shortcut icon" href="images/favicon/favicon.ico" type="image/x-icon">
<link rel="apple-touch-icon" href="images/favicon/favicon_180.png" sizes="180x180">
<link rel="icon" href="images/favicon/favicon_32.png" sizes="32x32" type="image/png">
<link rel="icon" href="images/favicon/favicon_48.png" sizes="48x48" type="image/png">
<link rel="icon" href="images/favicon/favicon_96.png" sizes="96x96" type="image/png">
<link rel="icon" href="images/favicon/favicon_144.png" sizes="144x144" type="image/png">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,200..900;1,200..900&display=swap" rel="stylesheet">
<link rel="canonical" href="https://swiftrocks.com/how-dynamicmemberlookup-works-internally-in-swift"/>
<!-- Bootstrap CSS Plugins -->
<link rel="stylesheet" type="text/css" href="css/bootstrap.css">
<!-- Prism CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/prism5.css">
<!-- Main CSS Stylesheet -->
<link rel="stylesheet" type="text/css" href="css/style49.css">
<link rel="stylesheet" type="text/css" href="css/sponsor5.css">
<!-- HTML5 shiv and Respond.js support IE8 or Older for HTML5 elements and media queries -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "BlogPosting",
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://swiftrocks.com/how-dynamicmemberlookup-works-internally-in-swift"
},
"image": [
"https://swiftrocks.com/images/thumbs/thumb_dark.jpg"
],
"datePublished": "2018-12-06T13:42:07+00:00",
"dateModified": "2020-04-12T14:00:00+02:00",
"author": {
"@type": "Person",
"name": "Bruno Rocha"
},
"publisher": {
"@type": "Organization",
"name": "SwiftRocks",
"logo": {
"@type": "ImageObject",
"url": "https://swiftrocks.com/images/thumbs/thumb_dark.jpg"
}
},
"headline": "How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)",
"abstract": "The @dynamicMemberLookup attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python."
}
</script>
</head>
<body>
<div id="main">
<!-- Blog Header -->
<!-- Blog Post (Right Sidebar) Start -->
<div class="container">
<div class="col-xs-12">
<div class="page-body">
<div class="row">
<div><a class="logo-link" href="https://swiftrocks.com">
<img id="logo" class="logo" alt="SwiftRocks" src="images/bg/logo2dark.png">
</a>
<div class="menu-large">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-large">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
<div class="menu-small">
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-1">
<div class="menu-item">
<a href="blog">blog</a>
</div>
<div class="menu-item">
<a href="about">about</a>
</div>
<div class="menu-item">
<a href="talks">talks</a>
</div>
<div class="menu-item">
<a href="projects">projects</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
<div class="menu-arrow-right"></div>
<div class="menu-header menu-header-small-2">
<div class="menu-item">
<a href="software-engineering-book-recommendations">book recs</a>
</div>
<div class="menu-item">
<a href="games">game recs</a>
</div>
<div class="menu-arrow-right-2"></div>
</div>
</div>
</div>
<div class="content-page" id="WRITEIT_DYNAMIC_CONTENT">
<!--WRITEIT_POST_NAME=How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)-->
<!--WRITEIT_POST_HTML_NAME=how-dynamicmemberlookup-works-internally-in-swift-->
<!--WRITEIT_POST_SITEMAP_DATE_LAST_MOD=2020-04-12T14:00:00+02:00-->
<!--WRITEIT_POST_SITEMAP_DATE=2018-12-06T13:42:07+00:00-->
<!--Add here the additional properties that you want each page to possess.-->
<!--These properties can be used to change content in the template page or in the page itself as shown here.-->
<!--Properties must start with 'WRITE_IT_POST'.-->
<!--Writeit provides and injects WRITEIT_POST_NAME and WRITEIT_POST_HTML_NAME by default.-->
<!--WRITEIT_POST_SHORT_DESCRIPTION=The @dynamicMemberLookup attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python.-->
<title>How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)</title>
<div class="blog-post">
<div class="post-title-index">
<h1>How @dynamicMemberLookup Works Internally in Swift (+ Creating Custom Swift Attributes)</h1>
</div>
<div class="post-info">
<div class="post-info-text">
Published on 06 Dec 2018
</div>
</div>
<p>The <code>@dynamicMemberLookup</code> attribute was introduced in Swift 4.2 to add a certain degree of dynamism into the language similar to what is seen in languages like Python. When applied to a type, properties from this type will be resolved in runtime, meaning that you can call things that don't necessarily exist like they were explicitly defined:</p>
<pre><code>@dynamicMemberLookup class MyClass {
subscript(dynamicMember input: String) -> String {
return input == "foo" ? "bar" : "SwiftRocks"
}
}
MyClass().foo // bar
MyClass().notFoo // SwiftRocks
// These properties don't exist, but they can be called because the type is @dynamicMemberLookup.</code></pre>
<div class="sponsor-article-ad-auto hidden"></div>
<p>As seen in the snippet, usage of this attribute instead forces the type to provide a <code>dynamicMember</code> subscript which receives the "fake" property name as a parameter and acts upon it.</p>
<p><a href="https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md">The original motivation</a> was that it could be used in interoperability layers with dynamic languages, allowing you to call Python code in Swift like you would in Python itself, for example. Although this attribute doesn't have pure Swift in mind, you can certainly use it for it. I would probably never use it in regular iOS development, but my favorite use case is being able to improve JSON parsing:</p>
<pre><code>let data: String? = dict["data"] as? String
//
let data: String? = dict.data //dynamically search for "data"</code></pre>
<p>In a similar fashion, <code>@dynamicCallable</code> was added in Swift 5.0 as a follow-up to add the ability to dynamically call a method from a dynamicCallable type:</p>
<pre><code>let myType: MyDynamicType = MyDynamicType()
myType(someArg2: someVal, someArg2: someVal2)</code></pre>
<p>I was interested in learning how the attributes worked inside the compiler to know more about how they are able to transform these fake expressions into legit ones, so I've <i>once again</i> reverse-engineered the Swift compiler to find these answers - and used this knowledge to create my own Swift attribute.</p>
<p><i>Disclaimer: As always, this is a result of my own research and reverse-engineering for the pure purpose of learning something new. As I obviously have nothing to do with the original development of these attributes, some assumptions might not be fully correct. Feel free to correct me if you know how the compiler works!</i></p>
<p>This article will focus on the internals of <code>@dynamicMemberLookup</code>. <code>@dynamicCallable</code> works a bit differently, but overall follows the same idea.</p>
<h2>Attribute Declaration/Parsing</h2>
<p>A quick repo search shows us that in Swift, all attributes are mainly defined in the <a href="https://github.com/apple/swift/blob/master/include/swift/AST/Attr.def">Attr.def</a> file. Here's the definition of <code>@dynamicMemberLookup</code>:</p>
<pre><code>SIMPLE_DECL_ATTR(dynamicMemberLookup, DynamicMemberLookup,
OnNominalType, 9)</code></pre>
<p>A "simple attribute" is an attribute that holds no data (unlike <code>@available</code> which contains arguments). The first argument here is the name of the attribute in source files, the second one is the name of the attribute to be used inside the compiler (which will resolve to <code>DynamicMemberLookupAttr</code>), the third one defines its scope (in this case, a <code>NominalType</code> refers to the congregate of classes, structs, enums and protocols), and the last one is an unique code used internally to determine a valid attribute.</p>
<p>The first contact the Swift compiler has with attributes is during parsing. After lexical analysis generates the tokenized version of your code, the Parser will step through these tokens in order to generate a basic Abstract Syntax Tree (your code in a tree-like structure) which will later be used to further "understand" your code once typechecking is performed. This process can fail if it finds something it didn't expect - a mistyped keyword, an attribute in the wrong place, and so on. You have certainly seen this before in the shape of a "expected X identifier" error.</p>
<p>You can tell the compiler to print the Parser's resulting AST by running <code>swiftc -dump-parse</code>, but unfortunately for article purposes that doesn't print a type's attributes (a pull request opportunity?). But the good news is that we can confirm this by looking at the code that parses declaractions. The Parser does a gigantic amount of things, so I'll cheery-pick the relevant backtrace:</p>
<p><a href="https://github.com/apple/swift/blob/facaad18732fd476c515c8b58dd8ccd964148c54/lib/Parse/ParseStmt.cpp#L387-L391">ParseStmt.cpp:387-391</a> - if the current token represents a declaration, attempt to parse it.</p>
<pre><code>if isStartOfDecl()
parseDecl(...)</code></pre>
<p><a href="https://github.com/apple/swift/blob/facaad18732fd476c515c8b58dd8ccd964148c54/lib/Parse/ParseDecl.cpp#L2639-L2710">ParseDecl.cpp:2639-2710</a> - For a regular declaraction expression the attributes should be the first tokens, so they are the first tokens to be parsed:</p>
<pre><code>Parser::parseDecl(ParseDeclOptions Flags, llvm::function_ref<void(Decl*)> Handler) {
//removed: dealing with #if/#warning/#error
parseDeclAttributeList(...)
//rest of the parsing for a declaration
}</code></pre>
<p>What <code>parseDeclAttributeList()</code> does is a <code>do while</code> loop to parse an attribute if the current token is an <code>@</code>, which then calls <a href="https://github.com/apple/swift/blob/facaad18732fd476c515c8b58dd8ccd964148c54/lib/Parse/ParseDecl.cpp#L1692">parseDeclAttribute()</a> to begin parsing the attribute:</p>
<pre><code>bool Parser::parseDeclAttribute(DeclAttributes &Attributes, SourceLoc AtLoc)
if (Tok.isNot(tok::identifier) &&
Tok.isNot(tok::kw_in) &&
Tok.isNot(tok::kw_inout)) {
diagnose(Tok, diag::expected_attribute_name); // Compiler error for "Expected attribute name"
return true;
}
DeclAttrKind DK = DeclAttribute::getAttrKindFromString(Tok.getText());
// FIXME: This renaming happened before Swift 3, we can probably remove
// the specific fallback path at some point.
checkInvalidAttrName("availability", "available", DAK_Available, diag::attr_renamed); // Checks if the attribute name matches the old name and fails, suggesting the new
// more checks for all renamed or deprecated attributes
if (DK == "a valid attribute from Attr.def") // line 1805
return parseNewDeclAttribute(Attributes, AtLoc, DK);
diagnose(Tok, diag::unknown_attribute, Tok.getText()); // Compilation error for "unknown attribute %@"
}</code></pre>
<p>I like this method because we can see how Swift treats renamed attributes - just explicitely check if the current token matches an old name and throw an error stating that it's now called something else. But in short, we're just seeing if the name of our attribute matches an attribute defined at <code>Attr.def</code>, halting compilation if that's not the case. If the attribute exists, <code>parseNewDeclAttribute</code> will consume the token and add it to an attributes list for that AST.</p>
<p>By running the Swift compiler with the <code>-dump-parse</code> attribute, we'll tell the compiler to start compiling but stop as soon as the parsing step ends. This allows us to confirm that this is indeed where this logic is being executed:</p>
<pre class="command-line language-bash" data-host="SwiftRocks"><code>swiftc -dump-parse attrs.swift</code></pre>
<pre><code>@swiftRocks class Foo {} // error: unknown attribute 'swiftRocks'
@availability class Foo {} // error: '@availability' has been renamed to '@available'</code></pre>
<h2>Intermission: Creating a new <code>@swiftRocks</code> attribute</h2>
<p>Before seeing how this attribute results in dynamic members, how about using this knowledge to actually make an attribute of our own?</p>
<p>This brief introduction shows us that the barebones of an attribute aren't that complicated at all, and we can use that information to create a basic <code>@swiftRocks</code> attribute.</p>
<p>For that, I'll just add an entry for a class attribute in <code>Attr.def</code>:</p>
<pre><code>SIMPLE_DECL_ATTR(swiftRocks, SwiftRocks, OnClass, 83)</code></pre>
<p>Doing so forced me to add my attribute to a few lists and add a <code>visitSwiftRocksAttr()</code> method in TypeCheckAttr.cpp, which I did but left it empty since my attribute does nothing at the moment:</p>
<pre><code>void AttributeChecker::
visitSwiftRocksAttr(SwiftRocksAttr *attr) {}</code></pre>
<p>This is enough to make a <code>@swiftRocks</code> type compile, although nothing will happen since there's no logic tied to it. To see something happen, I'll pretend that older Swift versions used this very useful attribute as <code>@rockingSwift</code> by adding a new check at <code>parseDeclAttribute</code>: </p>
<pre><code>checkInvalidAttrName("rockingSwift", "swiftRocks", DAK_SwiftRocks, diag::attr_renamed);</code></pre>
<pre><code>@rockingSwift class Foo {} //error: '@rockingSwift' has been renamed to '@swiftRocks'</code></pre>
<p>We'll get back to it later.</p>
<h2>Defining/changing behaviour based on attributes</h2>
<p>After parsing, <code>@dynamicMemberLookup</code> will come to play again during semantic analysis. In order to confirm that your code is legit, the compiler will annotate the AST's nodes with their respective types and confirm that they can do whatever it is that they are doing. Some debugging revealed that the typechecking of a declaration triggers a typechecking call for every attribute it contains - first to confirm that the attribute is on the correct type (in this case, a <code>NominalType</code>), and second in order for you to confirm that the attribute is being used correctly. The latter happens in the same place where I had to create my <code>visitSwiftRocksAttr</code> method, but in <code>visitDynamicMemberLookupAttr</code> instead. In short, this method checks if the type implements one or more valid <code>subscript(dynamicMember)</code>, and throws a compilation error if that's not the case:</p>
<pre><code>void AttributeChecker::
visitDynamicMemberLookupAttr(DynamicMemberLookupAttr *attr) {
// This attribute is only allowed on nominal types.
auto decl = cast<NominalTypeDecl>(D);
auto type = decl->getDeclaredType();
// Look up `subscript(dynamicMember:)` candidates.
auto subscriptName = DeclName(TC.Context, DeclBaseName::createSubscript(),
TC.Context.Id_dynamicMember);
auto candidates = TC.lookupMember(decl, type, subscriptName);
// If there are no candidates, then the attribute is invalid.
if (candidates.empty()) {
TC.diagnose(attr->getLocation(), diag::invalid_dynamic_member_lookup_type,
type);
attr->setInvalid();
return;
}
// If no candidates are valid, then reject one.
auto oneCandidate = candidates.front();
candidates.filter([&](LookupResultEntry entry, bool isOuter) -> bool {
auto cand = cast<SubscriptDecl>(entry.getValueDecl());
TC.validateDeclForNameLookup(cand);
return isValidDynamicMemberLookupSubscript(cand, decl, TC);
});
if (candidates.empty()) {
TC.diagnose(oneCandidate.getValueDecl()->getLoc(),
diag::invalid_dynamic_member_lookup_type, type);
attr->setInvalid();
}
}</code></pre>
<p>As far as developing attributes goes, the standard seems to end here. Because attributes can be used for virtually anything, each attribute is developed where it makes sense for it. In <code>@dynamicMemberLookup</code>'s case, this happens during semantic analysis - when the Constraint System fails to resolve our unexistent properties through normal means, checking the existence of this attribute is used as a last resort: (simplified for readability purposes, <a href="https://github.com/apple/swift/blob/1c82d4977d4d58b93a46f3e8022e356862f98d57/lib/Sema/CSSimplify.cpp#L3592">original method here</a>)</p>
<pre><code>MemberLookupResult ConstraintSystem::
performMemberLookup(...) {
//Removed: Attempt resolve member through several means, but fail since the property doesn't exist
// If we're about to fail lookup, but we are looking for members in a type
// with the @dynamicMemberLookup attribute, then we resolve a reference
// to a `subscript(dynamicMember:)` method and pass the member name as a
// string parameter.
if (cantResolveIt && isSimpleName) {
auto name = memberName.getBaseIdentifier();
if (hasDynamicMemberLookupAttribute(...)) {
auto &ctx = getASTContext();
// Find `subscript(dynamicMember:)` methods in this type.
auto subscriptName = DeclName(ctx, DeclBaseName::createSubscript(), ctx.Id_dynamicMember);
auto subscripts = performMemberLookup(constraintKind,
subscriptName,
baseTy, functionRefKind,
memberLocator,
includeInaccessibleMembers);
for (auto candidate : subscripts.ViableCandidates) {
auto decl = cast<SubscriptDecl>(candidate.getDecl());
if (isValidDynamicMemberLookupSubscript(decl, DC, TC))
result.addViable(OverloadChoice::getDynamicMemberLookup(baseTy, decl, name));
}
}
}</code></pre>
<p>By confirming that the fake property comes from a type that uses the attribute (remember that the attribute was added to the declaration's AST), the solver concludes that it's possible to resolve it by overloading it with the type's <code>subscript(dynamicMember:)</code> declaration.</p>
<p>After the CS resolves the intended return type of the property, the Sema's Solution Application phase will detect the desired overload solution and generate a <code>subscript</code> expression that matches the original definition inside the type. Finally, this expression replaces the original property call. <a href="https://github.com/apple/swift/blob/1c82d4977d4d58b93a46f3e8022e356862f98d57/lib/Sema/CSApply.cpp#L2752">(original file here)</a></p>
<pre><code>case OverloadChoiceKind::DynamicMemberLookup: {
// Application of a DynamicMemberLookup result turns a member access of
// x.foo into x[dynamicMember: "foo"].
// Removed for readability
// Generate a (dynamicMember: T) expression.
auto fieldName = selected.choice.getName().getBaseIdentifier().str();
auto index = buildDynamicMemberLookupIndexExpr(fieldName, ...);
// Build and return a subscript that uses this string as the index.
return buildSubscript(base, index, ctx.Id_dynamicMember, ...)
}</code></pre>
<p>As spoiled by the comment above, this means that <code>@dynamicMemberLookup</code> properties are just syntax sugars for subscript calls! Because our fake properties really don't exist, the compiler swaps them with calls to the subscript method required by the attribute.</p>
<p>You can confirm this by compiling with the <code>-dump-ast</code> argument. Similar to <code>-dump-parse</code>, this argument will stop the compilation after typechecking is performed, allowing you to see the complete version of the AST. For <code>let foo: String = myType.bar</code>, the result will be something like this:</p>
<pre><code>(pattern_named type='String' 'foo')
(subscript_expr type='String'
(tuple_expr implicit type='(dynamicMember: String)' names=dynamicMember
(string_literal_expr implicit type='String' value="bar")))</code></pre>
<p>...which vaguely means <code>let foo: String = myType[dynamicMember: "bar"]</code>.</p>
<h2>Bonus: Adding functionality to <code>@swiftRocks</code></h2>
<h3>Adding requirements</h3>
<p>Now that <code>@dynamicMemberLookup</code> is uncovered, we're ready to make our custom attribute <i>actually</i> do something.</p>
<p>The first thing I want to change is the checker function I had to add when the attribute was created. I want this attribute to work only in classes that are called <code>ClassThatRocks</code>. If that's not the case, compilation must fail.</p>
<p>To be able to do that, I added a new identifier called <code>id_ClassThatRocks</code> to the <a href="https://github.com/apple/swift/blob/cc329fee03d6d6a30eb526e4be28bb876bd6ba0f/include/swift/AST/KnownIdentifiers.def">compiler's list of known identifiers</a>, and a "not ClassThatRocks" error to the <a href="https://github.com/apple/swift/blob/f871b0e661bb951b8943d235694f382921aa9994/include/swift/AST/DiagnosticsSema.def">compiler's list of semantic analysis related errors:</a></p>
<pre><code>ERROR(invalid_swiftrocks_name,none,
"@swiftRocks requires %0 to be called 'ClassThatRocks'", (Type))</code></pre>
<p>With that in place, I just need to compare the declaration's name in <code>visitSwiftRocksAttr()</code>:</p>
<pre><code>void AttributeChecker::
visitSwiftRocksAttr(SwiftRocksAttr *attr) {
auto decl = cast<NominalTypeDecl>(D);
auto type = decl->getDeclaredType();
if (decl->getName() != TC.Context.Id_ClassThatRocks) {
TC.diagnose(attr->getLocation(),
diag::invalid_swiftrocks_name, type);
attr->setInvalid();
}
}</code></pre>
<p>And the result is:</p>
<pre><code>@swiftRocks class Foo {} //error: @swiftRocks requires 'Foo' to be called 'ClassThatRocks'
@swiftRocks class ClassThatRocks {} //Works!</code></pre>
<h3>Adding the actual functionality: Wholesome reminders</h3>
<p>For the actual use, I've thought that such an incredible attribute should have an equally incredible use: When applied to a type, the compiler will put a warning on all properties that don't have "ThatRocks" in their name, because they are doing a good job and deserve recognition.</p>
<p>To do this, I'll intercept the typechecker in order to have access to all getter declarations. Given a getter, I can recursively its parents to see if someone has a <code>@swiftRocks</code> attribute and check if the getter's name doesn't contain "ThatRocks" in order to send the coder a friendly warning.</p>
<p>After a very long time of searching for suitable places for this implementation, I've found that <code>typeCheckDecl()</code> has all the information I need. It's probably a terrible place to do this, but the members of SwiftRocks unanimously decided that this attribute is more important than coding practices. After another very long time of trying to figure out how to retrieve a getter's "type tree", here's what I ended up with</p>
<pre><code>void TypeChecker::typeCheckDecl(Decl *D) {
if (auto AD = dyn_cast<AccessorDecl>(D)) {
DeclName name = AD->getStorage()->getFullName();
if (auto nominal = D->getDeclContext()->getSelfNominalTypeDecl()) {
auto type = nominal->getDeclaredType();
if (name.isSimpleName() && !name.isSpecial() && hasSwiftRocksAttribute(type)) {
StringRef rocks = "ThatRocks";
StringRef strName = name.getBaseIdentifier().str();
if (!strName.contains(rocks)) {
diagnose(AD->getLoc(),
diag::invalid_swiftrocks_property_name,
strName);
}
}
}
}
//removed: rest of the method
}</code></pre>
<p>I'll spare you the details of <code>hasSwiftRocksAttribute()</code> because I just copied <code>hasDynamicMemberLookupAttribute()</code> and changed the attribute name, but it checks a type's parents until it find the attribute. <a href="https://github.com/apple/swift/blob/bf2a4712ecafbcaa364579dd762e3f7398331bf9/lib/Sema/CSSimplify.cpp#L3110">Here's the original one if you're curious.</a></p>
<p>After building the compiler and running the following snippet, all properties of <code>AwesomeClass</code> get their hardwork recognized!</p>
<pre><code>@swiftRocks class AwesomeClass {
let number: Int = 1 //warning: Property 'number' is doing its best. Consider naming it 'numberThatRocks'.
let stringThatRocks: String = "stringy"
}</code></pre>
<h2>Conclusion</h2>
<div class="sponsor-article-ad-auto hidden"></div>
<p>I enjoy researching these features because they tell you a lot about how the language works. In this case, we can see that attributes have infinite possibilities - from stupid name checks to making properties pop from thin air. One might argue that they aren't "swifty" compared to the rest of the language, but they'll likely continue to be an integral part of the language for years to come.</p>
<p>Follow me on my Twitter - <a href="https://twitter.com/rockbruno_">@rockbruno_</a>, and let me know of any suggestions and corrections you want to share.</p>
<h2>References and Good reads</h2>
<a href="https://github.com/apple/swift-evolution/blob/master/proposals/0195-dynamic-member-lookup.md">SE-0195 - @dynamicMemberLookup</a>
<br>
<a href="https://github.com/apple/swift/pull/14546">Original implementation of SE-0195</a>
<br>
<a href="https://github.com/apple/swift/blob/master/docs/TypeChecker.rst">Typechecker Docs</a>
<br>
<a href="https://github.com/apple/swift">The Swift Source Code</a>
<br>
</div></div>
<div class="blog-post footer-main">
<div class="footer-logos">
<a href="https://swiftrocks.com/rss.xml"><i class="fa fa-rss"></i></a>
<a href="https://hachyderm.io/@rockbruno"><svg class="svg-logo-link" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M433 179.1c0-97.2-63.7-125.7-63.7-125.7-62.5-28.7-228.6-28.4-290.5 0 0 0-63.7 28.5-63.7 125.7 0 115.7-6.6 259.4 105.6 289.1 40.5 10.7 75.3 13 103.3 11.4 50.8-2.8 79.3-18.1 79.3-18.1l-1.7-36.9s-36.3 11.4-77.1 10.1c-40.4-1.4-83-4.4-89.6-54a102.5 102.5 0 0 1 -.9-13.9c85.6 20.9 158.7 9.1 178.8 6.7 56.1-6.7 105-41.3 111.2-72.9 9.8-49.8 9-121.5 9-121.5zm-75.1 125.2h-46.6v-114.2c0-49.7-64-51.6-64 6.9v62.5h-46.3V197c0-58.5-64-56.6-64-6.9v114.2H90.2c0-122.1-5.2-147.9 18.4-175 25.9-28.9 79.8-30.8 103.8 6.1l11.6 19.5 11.6-19.5c24.1-37.1 78.1-34.8 103.8-6.1 23.7 27.3 18.4 53 18.4 175z"/></svg></a>
<a href="https://twitter.com/rockbruno_"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/rockbruno"><i class="fa fa-github"></i></a>
</div>
<div class="footer-text">
© 2025 Bruno Rocha
</div>
<div class="footer-text">
<p><a href="https://swiftrocks.com">Home</a> / <a href="blog">See all posts</a></p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Blog Post (Right Sidebar) End -->
</div>
</div>
</div>
<!-- All Javascript Plugins -->
<script type="text/javascript" src="js/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
<script type="text/javascript" src="js/prism5.js"></script>
<!-- Main Javascript File -->
<script type="text/javascript" src="js/scripts30.js"></script>
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-H8KZTWSQ1R"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-H8KZTWSQ1R');
</script>
</body>
</html>