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

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

staged modules proposal (updated)

  • Property mode set to 100644
File size: 12.0 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; };
14export struct S1 *f() {
15 return (struct S1 *) s;
16}
17</pre>
18 </td>
19 <td>
20<pre>
21module M1;
22import M;
23import M2;
24export struct S1 {
25 struct S arrS[2];
26 struct S2 *s2;
27};
28export const int N = 5;
29</pre>
30 </td>
31 <td>
32<pre>
33module M2;
34import M1;
35export struct S2 {
36 int i;
37 struct S1 s1;
38};
39</pre>
40 </tr>
41 <tr>
42 <td>
43 <b>Stage 1:</b><br>
44 &emsp; Top-level names<br>
45 &emsp; (file only)<br>
46 </td>
47 <td>
48<pre>
49Module name: M
50Imports: M1
51Type definitions: S
52Variable definitions: s, f
53</pre>
54 </td>
55 <td>
56<pre>
57M1
58M, M2
59S1
60N
61</pre>
62 </td>
63 <td>
64<pre>
65M2
66M1
67S2
68
69</pre>
70 </td>
71 </tr>
72 <tr>
73 <td>
74 <b>Stage 2:</b><br>
75 &emsp; Scope resolution<br>
76 &emsp; (file + imports)<br>
77 </td>
78 <td>
79<pre>
80M -> S, s, f
81M1 -> S1, N
82--------
83Types: S, S1
84Variables: s, f, N
85</pre>
86 </td>
87 <td>
88<pre>
89M1 -> S1, N
90M -> S, s, f
91M2 -> S2
92--------
93Types: S, S1, S2
94Variables: s, f, N
95</pre>
96 </td>
97 <td>
98<pre>
99M2 -> S2
100M1 -> S1, N
101--------
102Types: S1, S2
103Variables: N
104</pre>
105 </td>
106 </tr>
107 <tr>
108 <td>
109 Stage 3:<br>
110 &emsp; Symbol resolution<br>
111 &emsp; and resolving transitive dependencies<br>
112 &emsp; (file + transitive import closure)<br>
113 </td>
114 <td>
115<pre>
116"needs definition" graph:
117 S <-\
118/-> S1 --/
119|-> N
120\-- s
121 f
122</pre>
123 </td>
124 <td>
125<pre>
126 S <-\
127 S1 --/
128 N
129</pre>
130 </td>
131 <td>
132<pre>
133 S <-\
134/-> S1 --/
135\-- S2
136</pre>
137 </tr>
138</table>
139
140## What's going on here?
141
142### User perspective:
143
144The module system runs after the preprocessor and before symbol resolution.
145
146To be a module, `module <identifier>;` must be the first declaration.
147
148`import <identifier>;` must be before all symbol definitions.
149
150If it is used, `export` must be the first keyword of a symbol definition.
151
152Only symbol definitions are allowed at top-level in modules &mdash; `struct S1;` is unnecessary, as all top-level symbols are already visible to each other.
153
154Exports make symbols visible to importers. A module implicitly imports itself. Inline functions cannot be exported (moved to a future extension).
155
156*Some of these restrictions can be modified through future extensions (see Language extensions).*
157
158### Stage 1:
159
160Stage 1 gathers useful information from a single module file. To support cyclic imports (M and M1 import each other), this stage cannot rely on other files.
161
162We lack a symbol table at this stage, so we can only parse context-free information. However, we can extract top-level names.
163
164Some additional information can be gathered at this stage (eg. `const`, field names), but it isn't required. We do note which top-level symbols are exported.
165
166*In order to parse the file without a symbol table, the first name of a function/global definition is treated as a type.*
167
168*This means we disallow implicit int return types (they have been disallowed since C99). Note expression statements are disallowed at top-level.*
169
170### Stage 2:
171
172Stage 2 gathers useful information from a module file and its direct imports. We gather import information from stage 1.
173
174Here, we determine which symbols are in scope (ie. build a symbol table).
175
176This stage lacks the ability to inspect symbol definitions, so expressions (eg. `x.y.z`) are left for stage 3 to resolve.
177
178Types are not fully resolvable due to array sizes, `decltype` and templates. The names of types can be resolved in C, but can be done in stage 3 instead.
179
180*Note: `auto` and `typeof` are not allowed at top-level in C23.*
181
182### Stage 3:
183
184Stage 3 can look up symbol definitions by following imports recursively (ie. can access the transitive import closure). We use information from stage 2.
185
186Here, we can resolve the rest of the AST by loading symbol definitions as needed (eg. S1 needs the definition of S, but only needs declaration of S2).
187
188If a cycle is detected while loading symbol definitions, we have a type of infinite size or an unresolvable ambiguity &mdash; produce an error diagnostic.
189
190To 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.
191
192To do this efficiently, expression parsing code would call lookup functions to go from name to symbol candidates.
193
194*The stitched modules approach is to output code that would be reparsed in the original compiler, which is simpler to implement.*
195
196*However, if the module system does not parse expressions, it has to make a conservative guess on reachable symbol definitions, which is less efficient.*
197
198## Language extensions
199
200There are a number of limitations with the proposed module system, which can be resolved with extensions:
201
202* global module fragment: Taking inspiration from C++20 modules, this allows us to use header files within our modules.
203* export import: This allows us to avoid having to write out every import, by having one import expand into multiple imports.
204* module namespacing: We can follow the same ABI as C++20 modules (ie. `name@module`). This helps us avoid linker symbol clashes.
205* export inline functions: Allow inline functions to be exported, which would expose the definition of the function to the importer.
206* import as module_name: Makes it so we qualify names as `module_name.S` for more control.
207* tagged exports: Some modules may need more implementation details than others. `export(tag)` means `import M(tag);` gets it.
208* relaxed import placement: import statements could be placed within function definitions.
209* alternative module names: What if we imported using the file path, similar to `#include` ?
210* constexpr: While not a module-specific feature, having this reduces our reliance on preprocessor directives.
211* declaration diagnostics: If a user writes `struct S1;` in a module file, provide a diagnostic instead of erroring out.
212* dependency files: similar to preprocessors, have the module system output a list of files it has analyzed.
213* ordering initializers: `import ordered M;` would mean M is initialized before this module.
214* escape hatches: There may be cases where the user needs to circumvent the module system. Let them.
215* generating header files: Provide a way to generate .h files from a module file so plain C can use libraries written with modules.
216* migration tools: By handling circular imports, we can support much of what .c/.h files do. So build an automatic migration tool.
217* code analysis tools: Modules make the link between symbol names explicit, so we can perform more complex analyses and refactorings.
218
219## Notes
220
221### Stage 3 does a lot of duplicated work
222
223For example, S1 needs the definition of S in all 3 modules. Another example is if we had inline functions.
224
225This information could be cached. We would need to consider how to handle staleness, dependency tracking, race conditions and duplicate symbol definitions.
226
227### Stage 2 vs stage 3
228
229Consider a codebase as a graph (DAG), where nodes are the symbols and directed edges are dependencies (either "needs definition" or "needs declaration").
230
231Stage 2 builds the symbol table, which is used to determine these edges. Stage 3 resolves the graph by following the edges as necessary.
232
233### What build optimizations does this enable?
234
235Each file within a stage can be processed in parallel.
236
237If recompiling earlier stages generates new information that importers don't use, we can skip recompiling importers.
238
239This fine-grained analysis could be accomplished by hashing symbol definitions, similar to Rust incremental compilation.
240
241### Extracting top-level names
242
243Extracting top-level names can have multiple edge cases. For example, the following is valid in gcc and clang:
244
245```
246struct Outer {
247 int i;
248 struct Inner {
249 int j;
250 } k;
251};
252
253struct Inner i;
254```
255
256Since C is not formally specified, it is infeasible to catch all cases (we aim to eventually produce a "semi-formalism" of the module system, however).
257
258Rather than trying to cover all cases, it is more practical to work towards a use-case &mdash; migrating existing C code to this module system.
259
260In that light, the edge case demonstrated above is a lesser priority, as it is used infrequently and is relatively easy to manually fix.
261
262Handling cases such as `export struct S { int i; } s;` (exports S and s) are more important in comparison, as this pattern is fairly common.
263
264### To reparse or not to reparse
265
266While Zig demonstrates that all 3 stages can be performed in a single pass, this likely requires rewriting large parts of the compiler.
267
268Instead, we can implement stages 1 and 2 as producing JSON objects, and have stages 2 and 3 reparse the module files instead of caching the tokens.
269The outputs of previous stages can be passed via the command line for simplicity.
270
271We can consider optimizing this at a later point, and measure speedups. But at this stage, getting functionality is more important than execution speed.
272
273### Reusing existing code
274
275Taking the idea of reparsing files even further, we can take the stitched modules approach &mdash; output code to be reparsed in the original compiler.
276This means the module system does not parse expressions.
277
278The naive approach of treating an expression like a black-box would force us to load all visible symbols, since we wouldn't know which symbols it uses.
279This is highly likely to result in a lot of false cycles, which would severly hamper functionality.
280
281A better approach is to extract a set of identifiers from the token stream of the expression, since an expression only uses symbols that it specifies.
282
283### Dependency tracking
284
285In a large codebase, only a few files change between compilations. We would like to avoid recompiling the codebase every time.
286
287The C preprocessor can output a dependency file (.d) that lists the files that have been inspected, which is later used to determine when to recompile.
288Similarly, each stage of the module system can also output a dependency file.
289
290Like Zig, we can go further by using hashes instead of just timestamps, and track dependencies at the granularity of top-level symbols instead of files.
291
292At this stage of implementation, we defer this work until later.
293
294### Comparison with Zig
295
296Zig is a systems language with cyclic imports. It performs all 3 stages in a single pass by lazily analyzing information.
297
298Building a module resolves expressions (stage 3), which needs a symbol table (stage 2), which means extracting information from imports (stage 1).
299If an expression needs a symbol definition from an import, then to parse the symbol definition you need its symbol table. Keep going until resolved.
300
301Since all symbols get resolved, a library or executable file can be generated directly without having to call the linker afterwards.
302This shows that having a module system allows a language to subsume responsibilities which would otherwise need to be delegated to a separate build system.
303
304Object files and intermediate representation are stored in a cache. Incremental compilation patches binaries with top-level declaration granularity.
305Zig's build system also uses a DAG of steps that run concurrently, and track compilation inputs (eg. compiler options) to get deterministic builds.
306
307While this would be useful, creating a build system is a huge engineering effort and outside the scope of this proposal.
308This proposal is concerned with adding language features that a module system would use, which may be leveraged by a build system in the future.
309
310*To highlight the challenge of creating a build system, many of Zig's features (eg. incremental compilation) are still considered experimental.*
311*Rust has spent almost a decade refining its incremental compilation architecture.*
312
313*A quick note about Rust: it also has a module and build system. A crate is considered one translation unit, which is aggressively optimized.*
314*This hampers incremental compilation. By contrast, Zig compiles multiple object files for a project, so it can do some form of separate compilation.*
Note: See TracBrowser for help on using the repository browser.