| 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 | export struct S1 *f() {
|
|---|
| 15 | return (struct S1 *) s;
|
|---|
| 16 | }
|
|---|
| 17 | </pre>
|
|---|
| 18 | </td>
|
|---|
| 19 | <td>
|
|---|
| 20 | <pre>
|
|---|
| 21 | module M1;
|
|---|
| 22 | import M;
|
|---|
| 23 | import M2;
|
|---|
| 24 | export struct S1 {
|
|---|
| 25 | struct S arrS[2];
|
|---|
| 26 | struct S2 *s2;
|
|---|
| 27 | };
|
|---|
| 28 | export const int N = 5;
|
|---|
| 29 | </pre>
|
|---|
| 30 | </td>
|
|---|
| 31 | <td>
|
|---|
| 32 | <pre>
|
|---|
| 33 | module M2;
|
|---|
| 34 | import M1;
|
|---|
| 35 | export 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 |   Top-level names<br>
|
|---|
| 45 |   (file only)<br>
|
|---|
| 46 | </td>
|
|---|
| 47 | <td>
|
|---|
| 48 | <pre>
|
|---|
| 49 | Module name: M
|
|---|
| 50 | Imports: M1
|
|---|
| 51 | Type definitions: S
|
|---|
| 52 | Variable definitions: s, f
|
|---|
| 53 | </pre>
|
|---|
| 54 | </td>
|
|---|
| 55 | <td>
|
|---|
| 56 | <pre>
|
|---|
| 57 | M1
|
|---|
| 58 | M, M2
|
|---|
| 59 | S1
|
|---|
| 60 | N
|
|---|
| 61 | </pre>
|
|---|
| 62 | </td>
|
|---|
| 63 | <td>
|
|---|
| 64 | <pre>
|
|---|
| 65 | M2
|
|---|
| 66 | M1
|
|---|
| 67 | S2
|
|---|
| 68 |
|
|---|
| 69 | </pre>
|
|---|
| 70 | </td>
|
|---|
| 71 | </tr>
|
|---|
| 72 | <tr>
|
|---|
| 73 | <td>
|
|---|
| 74 | <b>Stage 2:</b><br>
|
|---|
| 75 |   Scope resolution<br>
|
|---|
| 76 |   (file + imports)<br>
|
|---|
| 77 | </td>
|
|---|
| 78 | <td>
|
|---|
| 79 | <pre>
|
|---|
| 80 | M -> S, s, f
|
|---|
| 81 | M1 -> S1, N
|
|---|
| 82 | --------
|
|---|
| 83 | Types: S, S1
|
|---|
| 84 | Variables: s, f, N
|
|---|
| 85 | </pre>
|
|---|
| 86 | </td>
|
|---|
| 87 | <td>
|
|---|
| 88 | <pre>
|
|---|
| 89 | M1 -> S1, N
|
|---|
| 90 | M -> S, s, f
|
|---|
| 91 | M2 -> S2
|
|---|
| 92 | --------
|
|---|
| 93 | Types: S, S1, S2
|
|---|
| 94 | Variables: s, f, N
|
|---|
| 95 | </pre>
|
|---|
| 96 | </td>
|
|---|
| 97 | <td>
|
|---|
| 98 | <pre>
|
|---|
| 99 | M2 -> S2
|
|---|
| 100 | M1 -> S1, N
|
|---|
| 101 | --------
|
|---|
| 102 | Types: S1, S2
|
|---|
| 103 | Variables: N
|
|---|
| 104 | </pre>
|
|---|
| 105 | </td>
|
|---|
| 106 | </tr>
|
|---|
| 107 | <tr>
|
|---|
| 108 | <td>
|
|---|
| 109 | Stage 3:<br>
|
|---|
| 110 |   Symbol resolution<br>
|
|---|
| 111 |   and resolving transitive dependencies<br>
|
|---|
| 112 |   (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 |
|
|---|
| 144 | The module system runs after the preprocessor and before symbol resolution.
|
|---|
| 145 |
|
|---|
| 146 | To be a module, `module <identifier>;` must be the first declaration.
|
|---|
| 147 |
|
|---|
| 148 | `import <identifier>;` must be before all symbol definitions.
|
|---|
| 149 |
|
|---|
| 150 | If it is used, `export` must be the first keyword of a symbol definition.
|
|---|
| 151 |
|
|---|
| 152 | Only symbol definitions are allowed at top-level in modules — `struct S1;` is unnecessary, as all top-level symbols are already visible to each other.
|
|---|
| 153 |
|
|---|
| 154 | Exports 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 |
|
|---|
| 160 | Stage 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 |
|
|---|
| 162 | We lack a symbol table at this stage, so we can only parse context-free information. However, we can extract top-level names.
|
|---|
| 163 |
|
|---|
| 164 | Some 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 |
|
|---|
| 172 | Stage 2 gathers useful information from a module file and its direct imports. We gather import information from stage 1.
|
|---|
| 173 |
|
|---|
| 174 | Here, we determine which symbols are in scope (ie. build a symbol table).
|
|---|
| 175 |
|
|---|
| 176 | This stage lacks the ability to inspect symbol definitions, so expressions (eg. `x.y.z`) are left for stage 3 to resolve.
|
|---|
| 177 |
|
|---|
| 178 | Types 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 |
|
|---|
| 184 | Stage 3 can look up symbol definitions by following imports recursively (ie. can access the transitive import closure). We use information from stage 2.
|
|---|
| 185 |
|
|---|
| 186 | Here, 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 |
|
|---|
| 188 | If a cycle is detected while loading symbol definitions, we have a type of infinite size or an unresolvable ambiguity — produce an error diagnostic.
|
|---|
| 189 |
|
|---|
| 190 | 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.
|
|---|
| 191 |
|
|---|
| 192 | To 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 |
|
|---|
| 200 | There 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 |
|
|---|
| 223 | For example, S1 needs the definition of S in all 3 modules. Another example is if we had inline functions.
|
|---|
| 224 |
|
|---|
| 225 | This 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 |
|
|---|
| 229 | Consider a codebase as a graph (DAG), where nodes are the symbols and directed edges are dependencies (either "needs definition" or "needs declaration").
|
|---|
| 230 |
|
|---|
| 231 | Stage 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 |
|
|---|
| 235 | Each file within a stage can be processed in parallel.
|
|---|
| 236 |
|
|---|
| 237 | If recompiling earlier stages generates new information that importers don't use, we can skip recompiling importers.
|
|---|
| 238 |
|
|---|
| 239 | This fine-grained analysis could be accomplished by hashing symbol definitions, similar to Rust incremental compilation.
|
|---|
| 240 |
|
|---|
| 241 | ### Extracting top-level names
|
|---|
| 242 |
|
|---|
| 243 | Extracting top-level names can have multiple edge cases. For example, the following is valid in gcc and clang:
|
|---|
| 244 |
|
|---|
| 245 | ```
|
|---|
| 246 | struct Outer {
|
|---|
| 247 | int i;
|
|---|
| 248 | struct Inner {
|
|---|
| 249 | int j;
|
|---|
| 250 | } k;
|
|---|
| 251 | };
|
|---|
| 252 |
|
|---|
| 253 | struct Inner i;
|
|---|
| 254 | ```
|
|---|
| 255 |
|
|---|
| 256 | Since 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 |
|
|---|
| 258 | Rather than trying to cover all cases, it is more practical to work towards a use-case — migrating existing C code to this module system.
|
|---|
| 259 |
|
|---|
| 260 | In that light, the edge case demonstrated above is a lesser priority, as it is used infrequently and is relatively easy to manually fix.
|
|---|
| 261 |
|
|---|
| 262 | Handling 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 |
|
|---|
| 266 | While Zig demonstrates that all 3 stages can be performed in a single pass, this likely requires rewriting large parts of the compiler.
|
|---|
| 267 |
|
|---|
| 268 | Instead, 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.
|
|---|
| 269 | The outputs of previous stages can be passed via the command line for simplicity.
|
|---|
| 270 |
|
|---|
| 271 | We 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 |
|
|---|
| 275 | Taking the idea of reparsing files even further, we can take the stitched modules approach — output code to be reparsed in the original compiler.
|
|---|
| 276 | This means the module system does not parse expressions.
|
|---|
| 277 |
|
|---|
| 278 | The 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.
|
|---|
| 279 | This is highly likely to result in a lot of false cycles, which would severly hamper functionality.
|
|---|
| 280 |
|
|---|
| 281 | A 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 |
|
|---|
| 285 | In a large codebase, only a few files change between compilations. We would like to avoid recompiling the codebase every time.
|
|---|
| 286 |
|
|---|
| 287 | The 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.
|
|---|
| 288 | Similarly, each stage of the module system can also output a dependency file.
|
|---|
| 289 |
|
|---|
| 290 | Like 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 |
|
|---|
| 292 | At this stage of implementation, we defer this work until later.
|
|---|
| 293 |
|
|---|
| 294 | ### Comparison with Zig
|
|---|
| 295 |
|
|---|
| 296 | Zig is a systems language with cyclic imports. It performs all 3 stages in a single pass by lazily analyzing information.
|
|---|
| 297 |
|
|---|
| 298 | Building a module resolves expressions (stage 3), which needs a symbol table (stage 2), which means extracting information from imports (stage 1).
|
|---|
| 299 | If 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 |
|
|---|
| 301 | Since all symbols get resolved, a library or executable file can be generated directly without having to call the linker afterwards.
|
|---|
| 302 | This 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 |
|
|---|
| 304 | Object files and intermediate representation are stored in a cache. Incremental compilation patches binaries with top-level declaration granularity.
|
|---|
| 305 | Zig's build system also uses a DAG of steps that run concurrently, and track compilation inputs (eg. compiler options) to get deterministic builds.
|
|---|
| 306 |
|
|---|
| 307 | While this would be useful, creating a build system is a huge engineering effort and outside the scope of this proposal.
|
|---|
| 308 | This 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.*
|
|---|