| 1 | # Staged modules
|
|---|
| 2 |
|
|---|
| 3 | <table>
|
|---|
| 4 | <tr>
|
|---|
| 5 | <td>
|
|---|
| 6 | <b>Example code:</b><br>
|
|---|
| 7 | </td>
|
|---|
| 8 | <td>
|
|---|
| 9 | <pre>
|
|---|
| 10 | module M;
|
|---|
| 11 | import M1;
|
|---|
| 12 | export struct S1 s[N];
|
|---|
| 13 | export struct S { int i; };
|
|---|
| 14 | </pre>
|
|---|
| 15 | </td>
|
|---|
| 16 | <td>
|
|---|
| 17 | <pre>
|
|---|
| 18 | module M1;
|
|---|
| 19 | import M;
|
|---|
| 20 | import M2;
|
|---|
| 21 | export struct S1 {
|
|---|
| 22 | struct S arrS[2];
|
|---|
| 23 | struct S2 *s2;
|
|---|
| 24 | };
|
|---|
| 25 | export const int N = 5;
|
|---|
| 26 | </pre>
|
|---|
| 27 | </td>
|
|---|
| 28 | <td>
|
|---|
| 29 | <pre>
|
|---|
| 30 | module M2;
|
|---|
| 31 | import M1;
|
|---|
| 32 | export 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 |   Exported names<br>
|
|---|
| 42 |   (file only)<br>
|
|---|
| 43 | </td>
|
|---|
| 44 | <td>
|
|---|
| 45 | <pre>
|
|---|
| 46 | M
|
|---|
| 47 | M1
|
|---|
| 48 | S
|
|---|
| 49 | s
|
|---|
| 50 | </pre>
|
|---|
| 51 | </td>
|
|---|
| 52 | <td>
|
|---|
| 53 | <pre>
|
|---|
| 54 | M1
|
|---|
| 55 | M, M2
|
|---|
| 56 | S1
|
|---|
| 57 | N
|
|---|
| 58 | </pre>
|
|---|
| 59 | </td>
|
|---|
| 60 | <td>
|
|---|
| 61 | <pre>
|
|---|
| 62 | M2
|
|---|
| 63 | M1
|
|---|
| 64 | S2
|
|---|
| 65 |
|
|---|
| 66 | </pre>
|
|---|
| 67 | </td>
|
|---|
| 68 | </tr>
|
|---|
| 69 | <tr>
|
|---|
| 70 | <td>
|
|---|
| 71 | <b>Stage 2:</b><br>
|
|---|
| 72 |   Scope resolution<br>
|
|---|
| 73 |   and building a symbol table<br>
|
|---|
| 74 |   (file + imports)<br>
|
|---|
| 75 | </td>
|
|---|
| 76 | <td>
|
|---|
| 77 | <pre>
|
|---|
| 78 | M -> S, s
|
|---|
| 79 | M1 -> S1, N
|
|---|
| 80 | </pre>
|
|---|
| 81 | </td>
|
|---|
| 82 | <td>
|
|---|
| 83 | <pre>
|
|---|
| 84 | M1 -> S1, N
|
|---|
| 85 | M -> S, s
|
|---|
| 86 | M2 -> S2
|
|---|
| 87 | </pre>
|
|---|
| 88 | </td>
|
|---|
| 89 | <td>
|
|---|
| 90 | <pre>
|
|---|
| 91 | M2 -> S2
|
|---|
| 92 | M1 -> S1, N
|
|---|
| 93 | </pre>
|
|---|
| 94 | </td>
|
|---|
| 95 | </tr>
|
|---|
| 96 | <tr>
|
|---|
| 97 | <td>
|
|---|
| 98 | Stage 3:<br>
|
|---|
| 99 |   Symbol resolution in expressions<br>
|
|---|
| 100 |   and resolving transitive dependencies<br>
|
|---|
| 101 |   (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 |
|
|---|
| 131 | The module system runs after the preprocessor and before symbol resolution.
|
|---|
| 132 |
|
|---|
| 133 | To 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 |
|
|---|
| 139 | Only symbol definitions are allowed at top-level in modules — all top-level symbols are already visible to each other, so `struct S1;` is unnecessary.
|
|---|
| 140 |
|
|---|
| 141 | Exports 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 |
|
|---|
| 147 | Stage 1 is about gathering useful information from analyzing a single module file in isolation.
|
|---|
| 148 |
|
|---|
| 149 | At this stage, there is no symbol table, so we're limited to just exported names.
|
|---|
| 150 |
|
|---|
| 151 | In the example, first line is module name, followed by import names, type names and functions/globals.
|
|---|
| 152 |
|
|---|
| 153 | Some additional information can be gathered at this stage (eg. `const`, field names), but it's not very useful at this point — 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 |
|
|---|
| 161 | Stage 2 is about what can be done with the stage 1 outputs of imported files (a module file implicitly imports itself).
|
|---|
| 162 |
|
|---|
| 163 | This information allows us to determine which symbols are in scope, allowing us to resolve names.
|
|---|
| 164 |
|
|---|
| 165 | This 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 |
|
|---|
| 173 | Stage 3 allows access to stage 2 outputs of the transitive import closure (ie. all modules that are reachable by following imports recursively).
|
|---|
| 174 |
|
|---|
| 175 | This allows us to resolve expressions and other remaining details by loading symbol definitions as needed.
|
|---|
| 176 |
|
|---|
| 177 | If a cycle is detected while loading symbol definitions, we have a type of infinite size or an unresolvable ambiguity — produce an error diagnostic.
|
|---|
| 178 |
|
|---|
| 179 | To reduce file IO, modules are lazily loaded in as symbol definitions are needed — S1 doesn't need the definition of S2, so M2 isn't loaded in.
|
|---|
| 180 |
|
|---|
| 181 | To 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 |
|
|---|
| 193 | For example, S1 needs the definition of S in all 3 modules. Ideally we would fetch from a cache instead.
|
|---|
| 194 |
|
|---|
| 195 | Another example is inline functions. Some thought would need to be put in to figure out exactly how the cache would function.
|
|---|
| 196 |
|
|---|
| 197 | This 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 |
|
|---|
| 201 | Stage 2 determines which names are in scope, stage 3 takes it further.
|
|---|
| 202 |
|
|---|
| 203 | As a language becomes more powerful, the value of stage 2 diminishes because a lot of language features require having the symbol definition.
|
|---|
| 204 |
|
|---|
| 205 | Zig weaves both stages into a single pass. Names are resolved lazily and symbol definitions loaded lazily.
|
|---|
| 206 |
|
|---|
| 207 | So stage 2 can be thought of as a substage of stage 3.
|
|---|
| 208 |
|
|---|
| 209 | ### What build optimizations does this enable?
|
|---|
| 210 |
|
|---|
| 211 | Each file within a stage can be processed in parallel.
|
|---|
| 212 |
|
|---|
| 213 | If stage 1 doesn't change anything the later stages need, could skip recompiling importers.
|
|---|
| 214 |
|
|---|
| 215 | If stage 2 gets new symbols but doesn't use them, could also skip.
|
|---|
| 216 |
|
|---|
| 217 | This fine-grained analysis could be accomplished by hashing symbol definitions, similar to Rust incremental compilation.
|
|---|
| 218 |
|
|---|
| 219 | Contrast with Zig's top-down approach, which has a different set of advantages and disadvantages.
|
|---|
| 220 |
|
|---|
| 221 | ## Language extensions
|
|---|
| 222 |
|
|---|
| 223 | There 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.
|
|---|