Skip to content

Commit 2739159

Browse files
seritoolslnicola
authored andcommitted
Update 2021-11-21-ides-and-macros.adoc
Just a few grammar changes
1 parent efdd001 commit 2739159

File tree

1 file changed

+33
-33
lines changed

1 file changed

+33
-33
lines changed

blog/_posts/2021-11-21-ides-and-macros.adoc

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
:page-layout: post
55

66
In this article, we'll discuss challenges that language servers face when supporting macros.
7-
This is interesting, because rust-analyzer, macros are the hardest nut to crack.
7+
This is interesting, because for rust-analyzer, macros are the hardest nut to crack.
88

99
While we use Rust as an example, the primary motivation here is to inform future language design.
1010
As this is a case study rather than a thorough analysis, conclusions should be taken with a grain of salt.
@@ -36,7 +36,7 @@ fn make_S() -> S {
3636
}
3737
----
3838

39-
Here, a reasonable IDE feature (known as intention, code action, assist or just 💡) is to suggesting adding the rest of the fields to the struct literal:
39+
Here, a reasonable IDE feature (known as intention, code action, assist or just 💡) is to suggest adding the rest of the fields to the struct literal:
4040

4141
[source,rust]
4242
----
@@ -60,11 +60,11 @@ reflect![
6060
];
6161
----
6262

63-
What the macro does here is just mirroring every token.
64-
IDE has no troubles expanding this macro.
63+
What the macro does here is just to mirror every token.
64+
The IDE has no troubles expanding this macro.
6565
It also understands that, in the expansion, the `y` field is missing, and that `y: todo!()` can be added to the _expansion_ as a fix.
66-
What the IDE can't do though, is to figure out what should be changed in the code that the user wrote to achieve that effect.
67-
Another interesting case to think about is what if the macro just encrypts all identifiers?
66+
What the IDE can't do, though, is to figure out what should be changed in the code that the user wrote to achieve that effect.
67+
Another interesting case to think about is: What if the macro just encrypts all identifiers?
6868

6969
This is where "`__disproportionally__ hard`" bit lies.
7070
In a batch compiler, code generally moves only forward through compilation phases.
@@ -85,22 +85,22 @@ async fn main() {
8585
What a user sees here is just a usual Rust function with some annotation attached.
8686
Clearly, everything should just work, right?
8787
But from an IDE point of view, this example isn't that different from the `reflect!` one.
88-
`tokio::main` is just an opaque code which takes the tokens of the source function as an input, and produces some tokens as an output, which then replace the original function.
88+
`tokio::main` is just an opaque bit of code which takes the tokens of the source function as an input, and produces some tokens as an output, which then replace the original function.
8989
It just _happens_ that the semantics of the original code is mostly preserved.
90-
Again, `tokio::main` _could_ have encrypted every identifier!.
90+
Again, `tokio::main` _could_ have encrypted every identifier!
9191

92-
So, to make thing appear to work, an IDE necessary involves heuristics in such cases.
92+
So, to make thing appear to work, an IDE necessarily involves heuristics in such cases.
9393
Some possible options are:
9494

9595
* Just completely ignore the macro.
96-
This make boring things like completion mostly work, but leads to semantic errors elsewhere.
97-
* Expand the macro, apply IDE features to expansion, and try heuristically lift them to the original source code
96+
This makes boring things like completion mostly work, but leads to semantic errors elsewhere.
97+
* Expand the macro, apply IDE features to the expansion, and try to heuristically lift them to the original source code
9898
(this is the bit where "`and now we just guess the private key used to encrypt an identifier`" conceptually lives).
9999
This is the pedantically correct approach, but it breaks most IDE features in minor and major ways.
100100
What's worse, the breakage is unexplainable to users: "`I just added an annotation to the function, why I don't get any completions?`"
101-
* In the semantic model, maintain both precisely analyzed expanded code, as well as heuristically analyzed source code.
101+
* In the semantic model, maintain both the precisely analyzed expanded code and the heuristically analyzed source code.
102102
When writing IDE features, try to intelligently use precise analysis from the expansion to augment knowledge about the source.
103-
This still doesn't solve all the problems, but solves most of them good enough such that the users now are completely befuddled by those rare cases where heuristics break down.
103+
This still doesn't solve all the problems, but solves most of them good enough such that the users are now completely befuddled by those rare cases where the heuristics break down.
104104

105105
.First Lesson
106106
[NOTE]
@@ -114,14 +114,14 @@ Avoid situations where what looks like normal syntax is instead an arbitrary lan
114114
== Parallel Name Resolution
115115

116116
_The second_ challenge is performance and phasing.
117-
Batch compilers typically compile all the code, so a natural solution of just expanding all the macros works.
117+
Batch compilers typically compile all the code, so the natural solution of just expanding all the macros works.
118118
Or rather, there isn't a problem at all here, you just write the simplest code to do the expansion and things just work.
119-
Situation for an IDE is quite different -- the main reason why IDE is capable of working with keystroke latency is that it cheats.
119+
The situation for an IDE is quite different -- the main reason why the IDE is capable of working with keystroke latency is that it cheats.
120120
It just doesn't look at the majority of the code during code editing, and analyses the absolute minimum to provide a completion widget.
121121
To be able to do so, an IDE needs help from the language to understand which parts of code can be safely ignored.
122122

123-
Read https://rust-analyzer.github.io/blog/2020/07/20/three-architectures-for-responsive-ide.html[this other article] to understand specific tricks IDE can employ here.
124-
The most powerful idea there is that generally IDE needs to know only about top-level names, and it doesn't need to look inside, e.g, function bodies most of the time.
123+
Read https://rust-analyzer.github.io/blog/2020/07/20/three-architectures-for-responsive-ide.html[this other article] to understand specific tricks IDEs can employ here.
124+
The most powerful idea there is that, generally, an IDE needs to know only about top-level names, and it doesn't need to look inside e.g. function bodies most of the time.
125125
Ideally, an IDE processes all files in parallel, noting, for each file, which top-level names it contributes.
126126

127127
The problem with macros, of course, is that they can contribute new top-level names.
@@ -151,8 +151,8 @@ macro_rules! _declare_mod {
151151
pub(crate) use _declare_mod as declare_mod;
152152
----
153153

154-
Semantics like this is what prevents rust-analyzer to just process every file in isolation.
155-
Instead, there's a hard-to-parallelize and hard to make incremental bit in rust-analyzer, where we just accept high implementation complexity and poor runtime performance.
154+
Semantics like this are what prevents rust-analyzer to just process every file in isolation.
155+
Instead, there are bits in rust-analyzer that are hard to parallelize and hard to make incremental, where we just accept high implementation complexity and poor runtime performance.
156156

157157
There is an alternative -- design meta programming such that it can work "`file at a time`", and can be plugged into an embarrassingly parallel indexing phase.
158158
This is the design that Sorbet, a (very) fast type checker for Ruby chooses: https://youtu.be/Gdx6by6tcvw?t=804.
@@ -164,19 +164,19 @@ So let's make sure that the overall thing is still crazy fast, even if a particu
164164

165165
To flesh out this design bit:
166166

167-
* All macros used in a compilation unit must be know up-front.
168-
In particular, it's not possible to define a macro in one file of CU and use it in another.
167+
* All macros used in a compilation unit must be known up-front.
168+
In particular, it's not possible to define a macro in one file of a CU and use it in another.
169169
* Macros follow simplified name resolution rules, which are intentionally different from the usual ones to allow recognizing and expanding macros _before_ name resolution.
170-
For example, macro invocations could have a unique syntax, like `name!`, where `name` identifies a macro definition in the flat namespace of know-up-front macros.
171-
* Macros don't get to access anything outside of the file with macro invocation.
170+
For example, macro invocations could have a unique syntax, like `name!`, where `name` identifies a macro definition in the flat namespace of known-up-front macros.
171+
* Macros don't get to access anything outside of the file with the macro invocation.
172172
They _can_ simulate name resolution for identifiers within the file, but can't reach across files.
173173

174174
Here, limiting macros to local-only information is a conscious design choice.
175175
By limiting the power available to macros, we gain the properties we can use to make the tooling better.
176176
For example, a macro can't know a type of the variable, but because it can't do that, we know we can re-use macro expansion results when unrelated files change.
177177

178178
An interesting hack to regain the full power of type-inspecting macros is to move the problem from the language to the tooling.
179-
It is possible to run a code generation step before the build, which can use compiler as a library to do a global semantic analysis of the code written by the user.
179+
It is possible to run a code generation step before the build, which can use the compiler as a library to do a global semantic analysis of the code written by the user.
180180
Based on the analysis results, the tool can write some generated code, which would then be processed by IDEs as if it was written by a human.
181181

182182
.Second Lesson
@@ -199,15 +199,15 @@ macro_rules! m {
199199
m!(no);
200200
----
201201

202-
The behavior of command-line compiler here is to just die with out-of-memory error, and that's an OK behavior for this context.
202+
The behavior of the command-line compiler here is to just die with an out-of-memory error, and that's an OK behavior for this context.
203203
Of course it's better when the compiler gives a nice error message, but if it misbehaves and panics or loops infinitely on erroneous code, that is also OK -- the user can just `^C` the process.
204204

205205
For a long-running IDE process though, looping or eating all the memory is not an option -- all resources need to be strictly limited.
206206
This is especially important given that an IDE looks at incomplete and erroneous code most of the time, so it hits far more weird edge cases than a batch compiler.
207207

208208
Rust procedural macros are all-powerful, so rust-analyzer and IntelliJ Rust have to implement extra tricks to contain them.
209-
While `rustc` just loads proc-macro shared library into the process, IDEs load macros into a dedicated external process which can be killed without bringing the whole IDE down.
210-
Adding IPC to an otherwise purely-functional compiler code is technically challenging.
209+
While `rustc` just loads proc-macros as shared libraries into the process, IDEs load macros into a dedicated external process which can be killed without bringing the whole IDE down.
210+
Adding IPC to an otherwise purely functional compiler code is technically challenging.
211211

212212
A related problem is determinism.
213213
rust-analyzer assumes that all computations are deterministic, and it uses this fact to smartly forget about subsets of derived data, to save memory.
@@ -226,22 +226,22 @@ For a batch compiler, it's OK to go with optimistic best-effort guarantees: "`we
226226
IDEs have stricter availability requirements, so they have to be pessimistic: "`we cannot crash, so we assume that any macro is potentially non-deterministic`".
227227
====
228228

229-
Curiously, similar to the previous point, moving meta programming to a code generation build system step sidesteps the problem, as you again can optimistically assume determinism.
229+
Curiously, similar to the previous point, moving metaprogramming to a code generation build system step sidesteps the problem, as you again can optimistically assume determinism.
230230

231231
== Recap
232232

233-
When it comes to meta programming, IDEs are harder than the batch compilers.
234-
To paraphrase Kernighan, if you design meta programming in your compiler as cleverly as possible, you are not smart enough to write an IDE for it.
233+
When it comes to metaprogramming, IDEs have a harder time than the batch compilers.
234+
To paraphrase Kernighan, if you design metaprogramming in your compiler as cleverly as possible, you are not smart enough to write an IDE for it.
235235

236236
Some specific hard macro bits:
237237

238-
* In a compiler, code flows forward through compilation pipeline.
238+
* In a compiler, code flows forward through the compilation pipeline.
239239
IDE features generally flow _back_, from desugared code into the original source.
240240
Macros can easily make for an irreversible transformation.
241241

242-
* IDE is fast because it knows what to _not_ look at.
242+
* IDEs are fast because they know what to _not_ look at.
243243
Macros can hide what is there, and increase the minimum amount of work necessary to understand an isolated bit of code.
244244

245245
* User-written macros can crash.
246-
IDE can not crash.
246+
IDEs must not crash.
247247
Running macros from an IDE is therefore fun :-)

0 commit comments

Comments
 (0)