source: doc/proposals/modules-alvin/2_staged_modules/staged_modules.md@ cbd6b5c

Last change on this file since cbd6b5c was cbd6b5c, checked in by Alvin Zhang <alvin.zhang@…>, 3 months ago

staged modules proposal

  • Property mode set to 100644
File size: 7.4 KB
Line 
1# Staged modules
2
3<table>
4 <tr>
5 <td>
6 <b>Example code:</b><br>
7 </td>
8 <td>
9<pre>
10module M;
11import M1;
12export struct S1 s[N];
13export struct S { int i; };
14</pre>
15 </td>
16 <td>
17<pre>
18module M1;
19import M;
20import M2;
21export struct S1 {
22 struct S arrS[2];
23 struct S2 *s2;
24};
25export const int N = 5;
26</pre>
27 </td>
28 <td>
29<pre>
30module M2;
31import M1;
32export struct S2 {
33 int i;
34 struct S1 s1;
35};
36</pre>
37 </tr>
38 <tr>
39 <td>
40 <b>Stage 1:</b><br>
41 &emsp; Exported names<br>
42 &emsp; (file only)<br>
43 </td>
44 <td>
45<pre>
46M
47M1
48S
49s
50</pre>
51 </td>
52 <td>
53<pre>
54M1
55M, M2
56S1
57N
58</pre>
59 </td>
60 <td>
61<pre>
62M2
63M1
64S2
65
66</pre>
67 </td>
68 </tr>
69 <tr>
70 <td>
71 <b>Stage 2:</b><br>
72 &emsp; Scope resolution<br>
73 &emsp; and building a symbol table<br>
74 &emsp; (file + imports)<br>
75 </td>
76 <td>
77<pre>
78M -> S, s
79M1 -> S1, N
80</pre>
81 </td>
82 <td>
83<pre>
84M1 -> S1, N
85M -> S, s
86M2 -> S2
87</pre>
88 </td>
89 <td>
90<pre>
91M2 -> S2
92M1 -> S1, N
93</pre>
94 </td>
95 </tr>
96 <tr>
97 <td>
98 Stage 3:<br>
99 &emsp; Symbol resolution in expressions<br>
100 &emsp; and resolving transitive dependencies<br>
101 &emsp; (file + transitive import closure)<br>
102 </td>
103 <td>
104<pre>
105 S <-\
106/-> S1 --/
107|-> N
108\-- s
109</pre>
110 </td>
111 <td>
112<pre>
113 S <-\
114 S1 --/
115 N
116</pre>
117 </td>
118 <td>
119<pre>
120 S <-\
121/-> S1 --/
122\-- S2
123</pre>
124 </tr>
125</table>
126
127## What's going on here?
128
129### User perspective:
130
131The module system runs after the preprocessor and before symbol resolution.
132
133To be a module, `module <identifier>;` must be the first declaration.
134
135`import <identifier>;` must be before any symbol definitions.
136
137`export` must be the first keyword of a symbol definition.
138
139Only symbol definitions are allowed at top-level in modules &mdash; all top-level symbols are already visible to each other, so `struct S1;` is unnecessary.
140
141Exports make symbols visible to importers.
142
143*Some of these restrictions can be modified through future extensions (see Language extensions).*
144
145### Stage 1:
146
147Stage 1 is about gathering useful information from analyzing a single module file in isolation.
148
149At this stage, there is no symbol table, so we're limited to just exported names.
150
151In the example, first line is module name, followed by import names, type names and functions/globals.
152
153Some additional information can be gathered at this stage (eg. `const`, field names), but it's not very useful at this point &mdash; wait until stage 2.
154
155*In order to parse the file without knowing the symbol table beforehand, the first name of a function/global definition is treated as a type.*
156
157*This means implicit int return types are disallowed here (and have been since C99). Note expression statements are disallowed at top-level.*
158
159### Stage 2:
160
161Stage 2 is about what can be done with the stage 1 outputs of imported files (a module file implicitly imports itself).
162
163This information allows us to determine which symbols are in scope, allowing us to resolve names.
164
165This stage lacks the definitions of the imported symbols, so anything that requires those (eg. `x.y.z`) are left for stage 3 to resolve.
166
167*This means expressions are not parsed, so expression-dependent aspects of types are unresolved (eg. array sizes, `auto`, `typeof`, `decltype`, templates).*
168
169*Note: `auto` and `typeof` are not allowed at top-level in C23. Regardless, they can still be resolved in stage 3 by lazily loading their type information.*
170
171### Stage 3:
172
173Stage 3 allows access to stage 2 outputs of the transitive import closure (ie. all modules that are reachable by following imports recursively).
174
175This allows us to resolve expressions and other remaining details by loading symbol definitions as needed.
176
177If a cycle is detected while loading symbol definitions, we have a type of infinite size or an unresolvable ambiguity &mdash; produce an error diagnostic.
178
179To reduce file IO, modules are lazily loaded in as symbol definitions are needed &mdash; S1 doesn't need the definition of S2, so M2 isn't loaded in.
180
181To do this efficiently, expression parsing code would call lookup functions to go from name to symbol candidates.
182
183*The stitched modules approach is to output code that would be reparsed in the original compiler, which is simpler to implement.*
184
185*However, the module system does not parse expressions, so it would have to eagerly grab all reachable symbol definitions, which is inefficient.*
186
187*The point of breaking into stages is more about unlocking parallelization opportunities and avoiding duplicate work.*
188
189## Notes
190
191### Stage 3 does a lot of duplicated work
192
193For example, S1 needs the definition of S in all 3 modules. Ideally we would fetch from a cache instead.
194
195Another example is inline functions. Some thought would need to be put in to figure out exactly how the cache would function.
196
197This means stage 3 is grabbing information from a previous stage 3 run, so this is an optimization, not functionally required.
198
199### Why not merge stages 2 and 3 together?
200
201Stage 2 determines which names are in scope, stage 3 takes it further.
202
203As a language becomes more powerful, the value of stage 2 diminishes because a lot of language features require having the symbol definition.
204
205Zig weaves both stages into a single pass. Names are resolved lazily and symbol definitions loaded lazily.
206
207So stage 2 can be thought of as a substage of stage 3.
208
209### What build optimizations does this enable?
210
211Each file within a stage can be processed in parallel.
212
213If stage 1 doesn't change anything the later stages need, could skip recompiling importers.
214
215If stage 2 gets new symbols but doesn't use them, could also skip.
216
217This fine-grained analysis could be accomplished by hashing symbol definitions, similar to Rust incremental compilation.
218
219Contrast with Zig's top-down approach, which has a different set of advantages and disadvantages.
220
221## Language extensions
222
223There are a number of limitations with the proposed module system, which can be resolved with extensions:
224
225* global module fragment: Taking inspiration from C++20 modules, this allows us to use header files within our modules.
226* export import: This allows us to avoid having to write out every import, by having one import expand into multiple imports.
227* module namespacing: We can follow the same ABI as C++20 modules (ie. `name@module`). This helps us avoid linker symbol clashes.
228* import as module_name: Makes it so we qualify names as `module_name.S` for more control.
229* tagged exports: Some modules may need more implementation details than others. `export(tag)` means `import M(tag);` gets it.
230* relaxed import placement: import statements could be placed within function definitions.
231* alternative module names: What if we imported using the file path, similar to `#include` ?
232* constexpr: While not a module-specific feature, having this reduces our reliance on preprocessor directives.
233* ordering initializers: `import ordered M;` would mean M is initialized before this module.
234* escape hatches: There may be cases where the user needs to circumvent the module system. Let them.
235* migration tools: By handling circular imports, we can support much of what .c/.h files do. So build an automatic migration tool.
236* code analysis tools: Modules make the link between symbol names explicit, so we can perform more complex analyses and refactorings.
Note: See TracBrowser for help on using the repository browser.