Skip to content

Commit 50840fc

Browse files
committed
Zcu: refactor File
This commit makes some big changes to how we track state for Zig source files. In particular, it changes: * How `File` tracks its path on-disk * How AstGen discovers files * How file-level errors are tracked * How `builtin.zig` files and modules are created There is also one breaking change here, which is that `@import` of a non-existent module is now a compile error even if the import is not semantically analyzed. The original motivation here was to address incremental compilation bugs with the handling of files, such as #22696. To fix this, a few changes are necessary. Just like declarations may become unreferenced on an incremental update, meaning we suppress analysis errors associated with them, it is also possible for all imports of a file to be removed on an incremental update, in which case file-level errors for that file should be suppressed. As such, after AstGen, the compiler must traverse files (starting from analysis roots) and discover the set of "live files" for this update. Additionally, the compiler's previous handling of retryable file errors was not very good; the source location the error was reported as was based only on the first discovered import of that file. This source location also disappeared on future incremental updates. So, as a part of the file traversal above, we also need to figure out the source locations of imports which errors should be reported against. Another observation I made is that the "file exists in multiple modules" error was not implemented in a particularly good way (I get to say that because I wrote it!). It was subject to races, where the order in which different imports of a file were discovered affects both how errors are printed, and which module the file is arbitrarily assigned, with the latter in turn affecting which other files are considered for import. The thing I realised here is that while the AstGen worker pool is running, we cannot know for sure which module(s) a file is in; we could always discover an import later which changes the answer. So, here's how the AstGen workers have changed. We initially ensure that `zcu.import_table` contains the root files for all modules in this Zcu, even if we don't know any imports for them yet. Then, the AstGen workers do not need to be aware of modules. Instead, they simply ignore module imports, and only spin off more workers when they see a by-path import. During this phase, we work exclusively in absolute paths, to ensure consistency in terms of `NameTooLong` errors. After the AstGen workers all complete, we know that any file which might be imported is definitely in `import_table` and up-to-date. So, we perform a single-threaded graph traversal; similar to what `resolveReferences` plays for `AnalUnit`s, but for files instead. We figure out which files are alive, and which module each file is in. If a file turns out to be in multiple modules, we set a field on `Zcu` to indicate this error. If a file is in a different module to a prior update, we set a flag instructing `updateZirRefs` to invalidate all dependencies on the file. This traversal also discovers "import errors"; these are errors associated with a specific `@import`. There are two possible errors here: "module not found" when a module import uses an unmapped name, or "import outside of module root" when importing a file using `..` past the module root path. These errors have to be identified during this traversal because they depend on which module the file is in, which we don't know until now. For simplicity, `failed_files` now just maps to `?[]u8`, since the source location is always the whole file. In fact, this allows removing `LazySrcLoc.Offset.entire_file` completely, slightly simplifying some error reporting logic. File-level errors are now directly built in the `std.zig.ErrorBundle.Wip`. If the payload is not `null`, it is the message for a retryable error (i.e. an error loading the source file), and will be reported with a "file imported here" note pointing to the import site discovered during the single-threaded file traversal. The last piece of fallout here is how `Builtin` works. Rather than constructing "builtin" modules when creating `Package.Module`s, they are now constructed on-the-fly by `Zcu`. The map `Zcu.builtin_modules` maps from digests to `*Package.Module`s. These digests are abstract hashes of the `Builtin` value; i.e. all of the options which are placed into "builtin.zig". During the file traversal, we populate `builtin_modules` as needed, so that when we see this imports in Sema, we just grab the relevant entry from this map. This eliminates a bunch of awkward state tracking during construction of the module graph. It's also now clearer exactly what options the builtin module has, since previously it inherited some options arbitrarily from the first-created module with that "builtin" module! The actual effects of this commit are: * retryable file errors are now consistently reported against the whole file, with a note pointing to a live import of that file * incremental updates do not print retryable file errors differently between updates * incremental updates support files changing modules * incremental updates support files becoming unreferenced Resolves: #22696
1 parent aa82337 commit 50840fc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+1449
-1247
lines changed

build.zig

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,24 @@ const io = std.io;
88
const fs = std.fs;
99
const InstallDirectoryOptions = std.Build.InstallDirectoryOptions;
1010
const assert = std.debug.assert;
11-
const DevEnv = @import("src/dev.zig").Env;
1211
const ValueInterpretMode = enum { direct, by_name };
1312

1413
const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 15, .patch = 0 };
1514
const stack_size = 46 * 1024 * 1024;
1615

16+
/// Keep in sync with `Env` in `src/dev.zig`.
17+
const DevEnv = enum {
18+
bootstrap,
19+
core,
20+
full,
21+
c_source,
22+
ast_gen,
23+
sema,
24+
@"x86_64-linux",
25+
@"riscv64-linux",
26+
wasm,
27+
};
28+
1729
pub fn build(b: *std.Build) !void {
1830
const only_c = b.option(bool, "only-c", "Translate the Zig compiler to C code, with only the C backend enabled") orelse false;
1931
const target = b.standardTargetOptions(.{

lib/std/Build/Cache.zig

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -276,11 +276,13 @@ pub const HashHelper = struct {
276276
}
277277

278278
pub fn oneShot(bytes: []const u8) [hex_digest_len]u8 {
279+
return binToHex(oneShotBin(bytes));
280+
}
281+
282+
pub fn oneShotBin(bytes: []const u8) BinDigest {
279283
var hasher: Hasher = hasher_init;
280284
hasher.update(bytes);
281-
var bin_digest: BinDigest = undefined;
282-
hasher.final(&bin_digest);
283-
return binToHex(bin_digest);
285+
return hasher.finalResult();
284286
}
285287
};
286288

src/Builtin.zig

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ code_model: std.builtin.CodeModel,
1919
omit_frame_pointer: bool,
2020
wasi_exec_model: std.builtin.WasiExecModel,
2121

22+
/// Compute an abstract hash representing this `Builtin`. This is *not* a hash
23+
/// of the resulting file contents.
24+
pub fn hash(opts: @This()) [std.Build.Cache.bin_digest_len]u8 {
25+
var h: Cache.Hasher = Cache.hasher_init;
26+
inline for (@typeInfo(@This()).@"struct".fields) |f| {
27+
if (comptime std.mem.eql(u8, f.name, "target")) {
28+
// This needs special handling.
29+
std.hash.autoHash(&h, opts.target.cpu);
30+
std.hash.autoHash(&h, opts.target.os.tag);
31+
std.hash.autoHash(&h, opts.target.os.versionRange());
32+
std.hash.autoHash(&h, opts.target.abi);
33+
std.hash.autoHash(&h, opts.target.ofmt);
34+
std.hash.autoHash(&h, opts.target.dynamic_linker);
35+
} else {
36+
std.hash.autoHash(&h, @field(opts, f.name));
37+
}
38+
}
39+
return h.finalResult();
40+
}
41+
2242
pub fn generate(opts: @This(), allocator: Allocator) Allocator.Error![:0]u8 {
2343
var buffer = std.ArrayList(u8).init(allocator);
2444
try append(opts, &buffer);
@@ -263,22 +283,49 @@ pub fn append(opts: @This(), buffer: *std.ArrayList(u8)) Allocator.Error!void {
263283
}
264284
}
265285

266-
pub fn populateFile(comp: *Compilation, mod: *Module, file: *File) !void {
267-
if (mod.root.statFile(mod.root_src_path)) |stat| {
286+
/// This essentially takes the place of `Zcu.PerThread.updateFile`, but for 'builtin' modules.
287+
/// Instead of reading the file from disk, its contents are generated in-memory.
288+
pub fn populateFile(opts: @This(), gpa: Allocator, file: *File) Allocator.Error!void {
289+
assert(file.is_builtin);
290+
assert(file.status == .never_loaded);
291+
assert(file.source == null);
292+
assert(file.tree == null);
293+
assert(file.zir == null);
294+
295+
file.source = try opts.generate(gpa);
296+
297+
log.debug("parsing and generating 'builtin.zig'", .{});
298+
299+
file.tree = try std.zig.Ast.parse(gpa, file.source.?, .zig);
300+
assert(file.tree.?.errors.len == 0); // builtin.zig must parse
301+
302+
file.zir = try AstGen.generate(gpa, file.tree.?);
303+
assert(!file.zir.?.hasCompileErrors()); // builtin.zig must not have astgen errors
304+
file.status = .success;
305+
}
306+
307+
/// After `populateFile` succeeds, call this function to write the generated file out to disk
308+
/// if necessary. This is useful for external tooling such as debuggers.
309+
/// Assumes that `file.mod` is correctly set to the builtin module.
310+
pub fn updateFileOnDisk(file: *File) !void {
311+
assert(file.is_builtin);
312+
assert(file.status == .success);
313+
assert(file.source != null);
314+
315+
if (file.mod.?.root.statFile("builtin.zig")) |stat| {
268316
if (stat.size != file.source.?.len) {
269317
std.log.warn(
270-
"the cached file '{}{s}' had the wrong size. Expected {d}, found {d}. " ++
318+
"the cached file '{}{c}builtin.zig' had the wrong size. Expected {d}, found {d}. " ++
271319
"Overwriting with correct file contents now",
272-
.{ mod.root, mod.root_src_path, file.source.?.len, stat.size },
320+
.{ file.mod.?.root, std.fs.path.sep, file.source.?.len, stat.size },
273321
);
274-
275-
try writeFile(file, mod);
276322
} else {
277323
file.stat = .{
278324
.size = stat.size,
279325
.inode = stat.inode,
280326
.mtime = stat.mtime,
281327
};
328+
return;
282329
}
283330
} else |err| switch (err) {
284331
error.BadPathName => unreachable, // it's always "builtin.zig"
@@ -287,26 +334,13 @@ pub fn populateFile(comp: *Compilation, mod: *Module, file: *File) !void {
287334
error.NoDevice => unreachable, // it's not a pipe
288335
error.WouldBlock => unreachable, // not asking for non-blocking I/O
289336

290-
error.FileNotFound => try writeFile(file, mod),
337+
error.FileNotFound => {},
291338

292339
else => |e| return e,
293340
}
294341

295-
log.debug("parsing and generating '{s}'", .{mod.root_src_path});
296-
297-
file.tree = try std.zig.Ast.parse(comp.gpa, file.source.?, .zig);
298-
assert(file.tree.?.errors.len == 0); // builtin.zig must parse
299-
300-
file.zir = try AstGen.generate(comp.gpa, file.tree.?);
301-
assert(!file.zir.?.hasCompileErrors()); // builtin.zig must not have astgen errors
302-
file.status = .success;
303-
// Note that whilst we set `zir` here, we populated `path_digest`
304-
// all the way back in `Package.Module.create`.
305-
}
306-
307-
fn writeFile(file: *File, mod: *Module) !void {
308342
var buf: [std.fs.max_path_bytes]u8 = undefined;
309-
var af = try mod.root.atomicFile(mod.root_src_path, .{ .make_path = true }, &buf);
343+
var af = try file.mod.?.root.atomicFile("builtin.zig", .{ .make_path = true }, &buf);
310344
defer af.deinit();
311345
try af.file.writeAll(file.source.?);
312346
af.finish() catch |err| switch (err) {
@@ -331,6 +365,7 @@ fn writeFile(file: *File, mod: *Module) !void {
331365
const builtin = @import("builtin");
332366
const std = @import("std");
333367
const Allocator = std.mem.Allocator;
368+
const Cache = std.Build.Cache;
334369
const build_options = @import("build_options");
335370
const Module = @import("Package/Module.zig");
336371
const assert = std.debug.assert;

0 commit comments

Comments
 (0)