zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 7ca061f3d68ff936dd77a58402c817b0aa1044e6 (tree)
parent 986a4f1445e3ed1a6227a24d78b2154239ccdb31
Author: Matthew Lugg <mlugg@mlugg.co.uk>
Date:   Thu, 12 Feb 2026 23:33:04 +0000

compiler: rework and simplify main loop

Diffstat:
Msrc/Compilation.zig | 507++++---------------------------------------------------------------------------
Msrc/Sema.zig | 6+++---
Msrc/Zcu.zig | 2++
Msrc/Zcu/PerThread.zig | 580+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
4 files changed, 482 insertions(+), 613 deletions(-)

diff --git a/src/Compilation.zig b/src/Compilation.zig @@ -21,7 +21,6 @@ const introspect = @import("introspect.zig"); const link = @import("link.zig"); const tracy = @import("tracy.zig"); const trace = tracy.trace; -const traceNamed = tracy.traceNamed; const build_options = @import("build_options"); const LibCInstallation = std.zig.LibCInstallation; const glibc = @import("libs/glibc.zig"); @@ -89,6 +88,9 @@ framework_dirs: []const []const u8, /// These are only for DLLs dependencies fulfilled by the `.def` files shipped /// with Zig. Static libraries are provided as `link.Input` values. windows_libs: std.StringArrayHashMapUnmanaged(void), +/// The number of items in `windows_libs` which we have already built. All items at or after this +/// index will be built in `performAllTheWork`. +windows_libs_num_done: u32, version: ?std.SemanticVersion, libc_installation: ?*const LibCInstallation, skip_linker_dependencies: bool, @@ -126,8 +128,6 @@ oneshot_prelink_tasks: std.ArrayList(link.PrelinkTask), /// work is queued or not. queued_jobs: QueuedJobs, -work_queues: [2]std.Deque(Job), - /// These jobs are to invoke the Clang compiler to create an object file, which /// gets linked with the Compilation. c_object_work_queue: std.Deque(*CObject), @@ -954,51 +954,6 @@ pub const RcSourceFile = struct { extra_flags: []const []const u8 = &.{}, }; -const Job = union(enum) { - /// Given the generated AIR for a function, put it onto the code generation queue. - /// MLUGG TODO: because type resolution is no longer necessary, we can remove this now - /// If the backend does not support `Zcu.Feature.separate_thread`, codegen and linking happen immediately. - /// Before queueing this `Job`, increase the estimated total item count for both - /// `comp.zcu.?.codegen_prog_node` and `comp.link_prog_node`. - codegen_func: struct { - func: InternPool.Index, - /// The AIR emitted from analyzing `func`; owned by this `Job` in `gpa`. - air: Air, - }, - /// Queue a `link.ZcuTask` to emit this non-function `Nav` into the output binary. - /// MLUGG TODO: because type resolution is no longer necessary, we can remove this now - /// If the backend does not support `Zcu.Feature.separate_thread`, the task is run immediately. - /// Before queueing this `Job`, increase the estimated total item count for `comp.link_prog_node`. - link_nav: InternPool.Nav.Index, - /// Before queueing this `Job`, increase the estimated total item count for `comp.link_prog_node`. - update_line_number: InternPool.TrackedInst.Index, - /// The `AnalUnit`, which is *not* a `func`, must be semantically analyzed. - /// This may be its first time being analyzed, or it may be outdated. - /// If the unit is a function, a `codegen_func` job will be queued after analysis completes. - /// If the unit is a *test* function, an `analyze_func` job will also be queued. - analyze_unit: InternPool.AnalUnit, - /// The main source file for the module needs to be analyzed. - /// For every module which is an analysis root, analyze the main struct type of the module's - /// root source file. This is how semantic analysis begins. - analyze_roots, - - /// The value is the index into `windows_libs`. - windows_import_lib: usize, - - fn stage(job: *const Job) usize { - // Prioritize functions so that codegen can get to work on them on a - // separate thread, while Sema goes back to its own work. - return switch (job.*) { - .codegen_func => 0, - .analyze_unit => |unit| switch (unit.unwrap()) { - .func => 0, - else => 1, - }, - else => 1, - }; - } -}; - pub const CObject = struct { /// Relative to cwd. Owned by arena. src: CSourceFile, @@ -2274,7 +2229,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, .root_mod = options.root_mod, .config = options.config, .dirs = options.dirs, - .work_queues = @splat(.empty), .c_object_work_queue = .empty, .win32_resource_work_queue = .empty, .c_source_files = options.c_source_files, @@ -2308,6 +2262,7 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, .root_name = root_name, .sysroot = sysroot, .windows_libs = .empty, + .windows_libs_num_done = 0, .version = options.version, .libc_installation = libc_dirs.libc_installation, .compiler_rt_strat = compiler_rt_strat, @@ -2670,16 +2625,6 @@ pub fn create(gpa: Allocator, arena: Allocator, io: Io, diag: *CreateDiagnostic, } } - // Generate Windows import libs. - if (target.os.tag == .windows) { - const count = comp.windows_libs.count(); - for (0..count) |i| { - try comp.queueJob(.{ .windows_import_lib = i }); - } - // when integrating coff linker with prelink, the above `queueJob` will need to move - // to something in `dispatchPrelinkWork`, which must queue all prelink link tasks - // *before* we begin working on the main job queue. - } if (comp.wantBuildLibUnwindFromSource()) { comp.queued_jobs.libunwind = true; } @@ -2763,7 +2708,6 @@ pub fn destroy(comp: *Compilation) void { if (comp.zcu) |zcu| zcu.deinit(); comp.cache_use.deinit(io); - for (&comp.work_queues) |*work_queue| work_queue.deinit(gpa); comp.c_object_work_queue.deinit(gpa); comp.win32_resource_work_queue.deinit(gpa); @@ -4563,13 +4507,7 @@ fn performAllTheWork( comp: *Compilation, main_progress_node: std.Progress.Node, update_arena: Allocator, -) JobError!void { - defer if (comp.zcu) |zcu| { - zcu.codegen_task_pool.cancel(zcu); - // Regardless of errors, `comp.zcu` needs to update its generation number. - zcu.generation += 1; - }; - +) (Allocator.Error || Io.Cancelable)!void { const io = comp.io; // This is awkward: we don't want to start the timer until later, but we won't want to stop it @@ -4602,206 +4540,32 @@ fn performAllTheWork( misc_group.async(io, workerDocsWasm, .{ comp, main_progress_node }); } - if (comp.zcu) |zcu| { - const tracy_trace = traceNamed(@src(), "astgen"); - defer tracy_trace.end(); - - const zir_prog_node = main_progress_node.start("AST Lowering", 0); - defer zir_prog_node.end(); - - var timer = comp.startTimer(); - defer if (timer.finish(io)) |ns| { - comp.mutex.lockUncancelable(io); - defer comp.mutex.unlock(io); - comp.time_report.?.stats.real_ns_files = ns; - }; - - const gpa = comp.gpa; - - var astgen_group: Io.Group = .init; - defer astgen_group.cancel(io); - - // We cannot reference `zcu.import_table` after we spawn any `workerUpdateFile` jobs, - // because on single-threaded targets the worker will be run eagerly, meaning the - // `import_table` could be mutated, and not even holding `comp.mutex` will save us. So, - // build up a list of the files to update *before* we spawn any jobs. - var astgen_work_items: std.MultiArrayList(struct { - file_index: Zcu.File.Index, - file: *Zcu.File, - }) = .empty; - defer astgen_work_items.deinit(gpa); - // Not every item in `import_table` will need updating, because some are builtin.zig - // files. However, most will, so let's just reserve sufficient capacity upfront. - try astgen_work_items.ensureTotalCapacity(gpa, zcu.import_table.count()); - for (zcu.import_table.keys()) |file_index| { - const file = zcu.fileByIndex(file_index); - if (file.is_builtin) { - // This is a `builtin.zig`, so updating is redundant. However, we want to make - // sure the file contents are still correct on disk, since it can improve the - // debugging experience better. That job only needs `file`, so we can kick it - // off right now. - astgen_group.async(io, workerUpdateBuiltinFile, .{ comp, file }); - continue; - } - astgen_work_items.appendAssumeCapacity(.{ - .file_index = file_index, - .file = file, - }); - } - - // Now that we're not going to touch `zcu.import_table` again, we can spawn `workerUpdateFile` jobs. - for (astgen_work_items.items(.file_index), astgen_work_items.items(.file)) |file_index, file| { - astgen_group.async(io, workerUpdateFile, .{ - comp, file, file_index, zir_prog_node, &astgen_group, - }); - } - - // On the other hand, it's fine to directly iterate `zcu.embed_table.keys()` here - // because `workerUpdateEmbedFile` can't invalidate it. The different here is that one - // `@embedFile` can't trigger analysis of a new `@embedFile`! - for (0.., zcu.embed_table.keys()) |ef_index_usize, ef| { - const ef_index: Zcu.EmbedFile.Index = @enumFromInt(ef_index_usize); - astgen_group.async(io, workerUpdateEmbedFile, .{ - comp, ef_index, ef, - }); - } - - try astgen_group.await(io); - } - + defer if (comp.zcu) |zcu| zcu.codegen_task_pool.cancel(zcu); if (comp.zcu) |zcu| { const pt: Zcu.PerThread = .activate(zcu, .main); - defer pt.deactivate(); - - const gpa = zcu.gpa; - - // On an incremental update, a source file might become "dead", in that all imports of - // the file were removed. This could even change what module the file belongs to! As such, - // we do a traversal over the files, to figure out which ones are alive and the modules - // they belong to. - const any_fatal_files = try pt.computeAliveFiles(); - - // If the cache mode is `whole`, add every alive source file to the manifest. - switch (comp.cache_use) { - .whole => |whole| if (whole.cache_manifest) |man| { - for (zcu.alive_files.keys()) |file_index| { - const file = zcu.fileByIndex(file_index); - - switch (file.status) { - .never_loaded => unreachable, // AstGen tried to load it - .retryable_failure => continue, // the file cannot be read; this is a guaranteed error - .astgen_failure, .success => {}, // the file was read successfully - } - - const path = try file.path.toAbsolute(comp.dirs, gpa); - defer gpa.free(path); - - const result = res: { - try whole.cache_manifest_mutex.lock(io); - defer whole.cache_manifest_mutex.unlock(io); - if (file.source) |source| { - break :res man.addFilePostContents(path, source, file.stat); - } else { - break :res man.addFilePost(path); - } - }; - result catch |err| switch (err) { - error.OutOfMemory => |e| return e, - else => { - try pt.reportRetryableFileError(file_index, "unable to update cache: {s}", .{@errorName(err)}); - continue; - }, - }; - } - }, - .none, .incremental => {}, - } - - if (any_fatal_files or - zcu.multi_module_err != null or - zcu.failed_imports.items.len > 0 or - comp.alloc_failure_occurred) - { - // We give up right now! No updating of ZIR refs, no nothing. The idea is that this prevents - // us from invalidating lots of incremental dependencies due to files with e.g. parse errors. - // However, this means our analysis data is invalid, so we want to omit all analysis errors. - zcu.skip_analysis_this_update = true; - // Since we're skipping analysis, there are no ZCU link tasks. - comp.link_queue.finishZcuQueue(comp); - // Let other compilation work finish to collect as many errors as possible. - try misc_group.await(io); - comp.link_queue.wait(io); - return; - } - - if (comp.time_report) |*tr| { - tr.stats.n_reachable_files = @intCast(zcu.alive_files.count()); - } - - if (comp.config.incremental) { - const update_zir_refs_node = main_progress_node.start("Update ZIR References", 0); - defer update_zir_refs_node.end(); - try pt.updateZirRefs(); - } - try zcu.flushRetryableFailures(); - - // It's analysis time! Queue up our initial analysis. - try comp.queueJob(.analyze_roots); - - zcu.sema_prog_node = main_progress_node.start("Semantic Analysis", 0); - if (comp.bin_file != null) { - zcu.codegen_prog_node = main_progress_node.start("Code Generation", 0); - } - // We increment `pending_codegen_jobs` so that it doesn't reach 0 until after analysis finishes. - // That prevents the "Code Generation" node from constantly disappearing and reappearing when - // we're probably going to analyze more functions at some point. - assert(zcu.pending_codegen_jobs.swap(1, .monotonic) == 0); // don't let this become 0 until analysis finishes - } - // When analysis ends, delete the progress nodes for "Semantic Analysis" and possibly "Code Generation". - defer if (comp.zcu) |zcu| { - zcu.sema_prog_node.end(); - zcu.sema_prog_node = .none; - if (zcu.pending_codegen_jobs.fetchSub(1, .monotonic) == 1) { - // Decremented to 0, so all done. - zcu.codegen_prog_node.end(); - zcu.codegen_prog_node = .none; - } - }; - - if (comp.zcu) |zcu| { - if (!zcu.backendSupportsFeature(.separate_thread)) { - // Close the ZCU task queue. Prelink may still be running, but the closed - // queue will cause the linker task to exit once prelink finishes. The - // closed queue also communicates to `enqueueZcu` that it should wait for - // the linker task to finish and then run ZCU tasks serially. - comp.link_queue.finishZcuQueue(comp); + defer { + pt.deactivate(); + // Regardless of errors, `comp.zcu` needs to update its generation number. + zcu.generation += 1; } + try pt.update(main_progress_node, &decl_work_timer); } - if (comp.zcu != null) { - // Start the timer for the "decls" part of the pipeline (Sema, CodeGen, link). - decl_work_timer = comp.startTimer(); - } + comp.link_queue.finishZcuQueue(comp); - work: while (true) { - for (&comp.work_queues) |*work_queue| if (work_queue.popFront()) |job| { - try processOneJob(.main, comp, job); - continue :work; + // This has to happen after the main semantic analysis loop because it is possible for Sema to + // call `addLinkLib` and hence add more items to `comp.windows_libs`. + for (comp.windows_libs.keys()[comp.windows_libs_num_done..]) |link_lib| { + mingw.buildImportLib(comp, link_lib) catch |err| { + // TODO Surface more error details. + comp.lockAndSetMiscFailure( + .windows_import_lib, + "unable to generate DLL import .lib file for {s}: {t}", + .{ link_lib, err }, + ); }; - if (comp.zcu) |zcu| { - // If there's no work queued, check if there's anything outdated - // which we need to work on, and queue it if so. - if (try zcu.findOutdatedToAnalyze()) |outdated| { - try comp.queueJob(.{ .analyze_unit = outdated }); - continue; - } - zcu.sema_prog_node.end(); - zcu.sema_prog_node = .none; - } - break; } - - comp.link_queue.finishZcuQueue(comp); + comp.windows_libs_num_done = @intCast(comp.windows_libs.count()); // Main thread work is all done, now just wait for all async work. try misc_group.await(io); @@ -5032,121 +4796,6 @@ fn dispatchPrelinkWork(comp: *Compilation, main_progress_node: std.Progress.Node }; } -const JobError = Allocator.Error || Io.Cancelable; - -pub fn queueJob(comp: *Compilation, job: Job) !void { - try comp.work_queues[job.stage()].pushBack(comp.gpa, job); -} - -pub fn queueJobs(comp: *Compilation, jobs: []const Job) !void { - for (jobs) |job| try comp.queueJob(job); -} - -fn processOneJob(tid: Zcu.PerThread.Id, comp: *Compilation, job: Job) JobError!void { - switch (job) { - .codegen_func => |func| { - const zcu = comp.zcu.?; - const gpa = zcu.gpa; - var owned_air: ?Air = func.air; - defer if (owned_air) |*air| air.deinit(gpa); - - // Some linkers need to refer to the AIR. In that case, the linker is not running - // concurrently, so we'll just keep ownership of the AIR for ourselves instead of - // letting the codegen job destroy it. - const disown_air = zcu.backendSupportsFeature(.separate_thread); - - // Begin the codegen task. If the codegen/link queue is backed up, this might - // block until the linker is able to process some tasks. - const codegen_task = try zcu.codegen_task_pool.start(zcu, func.func, &owned_air.?, disown_air); - if (disown_air) owned_air = null; - - try comp.link_queue.enqueueZcu(comp, tid, .{ .link_func = codegen_task }); - }, - .link_nav => |nav_index| { - const zcu = comp.zcu.?; - const nav = zcu.intern_pool.getNav(nav_index); - if (nav.analysis != null) { - const unit: InternPool.AnalUnit = .wrap(.{ .nav_val = nav_index }); - if (zcu.failed_analysis.contains(unit) or zcu.transitive_failed_analysis.contains(unit)) { - comp.link_prog_node.completeOne(); - return; - } - } - assert(nav.status == .fully_resolved); - try comp.link_queue.enqueueZcu(comp, tid, .{ .link_nav = nav_index }); - }, - .update_line_number => |tracked_inst| { - try comp.link_queue.enqueueZcu(comp, tid, .{ .debug_update_line_number = tracked_inst }); - }, - .analyze_unit => |unit| { - const tracy_trace = traceNamed(@src(), "analyze_unit"); - defer tracy_trace.end(); - - const pt: Zcu.PerThread = .activate(comp.zcu.?, tid); - defer pt.deactivate(); - - const maybe_err: Zcu.SemaError!void = switch (unit.unwrap()) { - .@"comptime" => |cu| pt.ensureComptimeUnitUpToDate(cu), - .nav_ty => |nav| pt.ensureNavTypeUpToDate(nav, null), - .nav_val => |nav| pt.ensureNavValUpToDate(nav, null), - .type_layout => |ty| pt.ensureTypeLayoutUpToDate(.fromInterned(ty), null), - .memoized_state => |stage| pt.ensureMemoizedStateUpToDate(stage, null), - .func => |func| pt.ensureFuncBodyUpToDate(func, null), - }; - maybe_err catch |err| switch (err) { - error.OutOfMemory => |e| return e, - error.Canceled => |e| return e, - error.AnalysisFail => return, - }; - - queue_test_analysis: { - if (!comp.config.is_test) break :queue_test_analysis; - const nav = switch (unit.unwrap()) { - .nav_val => |nav| nav, - else => break :queue_test_analysis, - }; - - // Check if this is a test function. - const ip = &pt.zcu.intern_pool; - if (!pt.zcu.test_functions.contains(nav)) { - break :queue_test_analysis; - } - - // Tests are always emitted in test binaries. The decl_refs are created by - // Zcu.populateTestFunctions, but this will not queue body analysis, so do - // that now. - try pt.zcu.ensureFuncBodyAnalysisQueued(ip.getNav(nav).status.fully_resolved.val); - } - }, - .analyze_roots => { - const tracy_trace = traceNamed(@src(), "analyze_roots"); - defer tracy_trace.end(); - - const zcu = comp.zcu.?; - const pt: Zcu.PerThread = .activate(zcu, tid); - defer pt.deactivate(); - for (zcu.analysisRoots()) |analysis_root_mod| { - const analysis_root_file = zcu.module_roots.get(analysis_root_mod).?.unwrap().?; - try pt.ensureFileAnalyzed(analysis_root_file); - } - }, - .windows_import_lib => |index| { - const tracy_trace = traceNamed(@src(), "windows_import_lib"); - defer tracy_trace.end(); - - const link_lib = comp.windows_libs.keys()[index]; - mingw.buildImportLib(comp, link_lib) catch |err| { - // TODO Surface more error details. - comp.lockAndSetMiscFailure( - .windows_import_lib, - "unable to generate DLL import .lib file for {s}: {t}", - .{ link_lib, err }, - ); - }; - }, - } -} - fn createDepFile(comp: *Compilation, dep_file: []const u8, bin_file: Cache.Path) anyerror!void { const io = comp.io; @@ -5474,112 +5123,6 @@ fn workerDocsWasmFallible(comp: *Compilation, prog_node: std.Progress.Node) SubU }; } -fn workerUpdateFile( - comp: *Compilation, - file: *Zcu.File, - file_index: Zcu.File.Index, - prog_node: std.Progress.Node, - group: *Io.Group, -) void { - const io = comp.io; - const tid: Zcu.PerThread.Id = .acquire(io); - defer tid.release(io); - - const child_prog_node = prog_node.start(fs.path.basename(file.path.sub_path), 0); - defer child_prog_node.end(); - - const pt: Zcu.PerThread = .activate(comp.zcu.?, tid); - defer pt.deactivate(); - pt.updateFile(file_index, file) catch |err| { - pt.reportRetryableFileError(file_index, "unable to load '{s}': {s}", .{ fs.path.basename(file.path.sub_path), @errorName(err) }) catch |oom| switch (oom) { - error.OutOfMemory => { - comp.mutex.lockUncancelable(io); - defer comp.mutex.unlock(io); - comp.setAllocFailure(); - }, - }; - return; - }; - - switch (file.getMode()) { - .zig => {}, // continue to logic below - .zon => return, // ZON can't import anything so we're done - } - - // Discover all imports in the file. Imports of modules we ignore for now since we don't - // know which module we're in, but imports of file paths might need us to queue up other - // AstGen jobs. - const imports_index = file.zir.?.extra[@intFromEnum(Zir.ExtraIndex.imports)]; - if (imports_index != 0) { - const extra = file.zir.?.extraData(Zir.Inst.Imports, imports_index); - var import_i: u32 = 0; - var extra_index = extra.end; - - while (import_i < extra.data.imports_len) : (import_i += 1) { - const item = file.zir.?.extraData(Zir.Inst.Imports.Item, extra_index); - extra_index = item.end; - - const import_path = file.zir.?.nullTerminatedString(item.data.name); - - if (pt.discoverImport(file.path, import_path)) |res| switch (res) { - .module, .existing_file => {}, - .new_file => |new| { - group.async(io, workerUpdateFile, .{ - comp, new.file, new.index, prog_node, group, - }); - }, - } else |err| switch (err) { - error.OutOfMemory => { - comp.mutex.lockUncancelable(io); - defer comp.mutex.unlock(io); - comp.setAllocFailure(); - }, - } - } - } -} - -fn workerUpdateBuiltinFile(comp: *Compilation, file: *Zcu.File) void { - Builtin.updateFileOnDisk(file, comp) catch |err| comp.lockAndSetMiscFailure( - .write_builtin_zig, - "unable to write '{f}': {s}", - .{ file.path.fmt(comp), @errorName(err) }, - ); -} - -fn workerUpdateEmbedFile(comp: *Compilation, ef_index: Zcu.EmbedFile.Index, ef: *Zcu.EmbedFile) void { - const io = comp.io; - const tid: Zcu.PerThread.Id = .acquire(io); - defer tid.release(io); - comp.detectEmbedFileUpdate(tid, ef_index, ef) catch |err| switch (err) { - error.OutOfMemory => { - comp.mutex.lockUncancelable(io); - defer comp.mutex.unlock(io); - comp.setAllocFailure(); - }, - }; -} - -fn detectEmbedFileUpdate(comp: *Compilation, tid: Zcu.PerThread.Id, ef_index: Zcu.EmbedFile.Index, ef: *Zcu.EmbedFile) !void { - const io = comp.io; - const zcu = comp.zcu.?; - const pt: Zcu.PerThread = .activate(zcu, tid); - defer pt.deactivate(); - - const old_val = ef.val; - const old_err = ef.err; - - try pt.updateEmbedFile(ef, null); - - if (ef.val != .none and ef.val == old_val) return; // success, value unchanged - if (ef.val == .none and old_val == .none and ef.err == old_err) return; // failure, error unchanged - - comp.mutex.lockUncancelable(io); - defer comp.mutex.unlock(io); - - try zcu.markDependeeOutdated(.not_marked_po, .{ .embed_file = ef_index }); -} - pub fn obtainCObjectCacheManifest( comp: *const Compilation, owner_mod: *Package.Module, @@ -8208,12 +7751,10 @@ pub fn addLinkLib(comp: *Compilation, lib_name: []const u8) !void { // If we haven't seen this library yet and we're targeting Windows, we need // to queue up a work item to produce the DLL import library for this. const gop = try comp.windows_libs.getOrPut(comp.gpa, lib_name); - if (gop.found_existing) return; - { + if (!gop.found_existing) { errdefer _ = comp.windows_libs.pop(); gop.key_ptr.* = try comp.gpa.dupe(u8, lib_name); } - try comp.queueJob(.{ .windows_import_lib = gop.index }); } /// This decides the optimization mode for all zig-provided libraries, including diff --git a/src/Sema.zig b/src/Sema.zig @@ -5362,7 +5362,7 @@ fn zirCImport(sema: *Sema, parent_block: *Block, inst: Zir.Inst.Index) CompileEr pt.updateFile(new_file_index, zcu.fileByIndex(new_file_index)) catch |err| return sema.fail(&child_block, src, "C import failed: {s}", .{@errorName(err)}); - try pt.ensureFileAnalyzed(new_file_index); + try pt.ensureFilePopulated(new_file_index); const ty: Type = .fromInterned(zcu.fileRootType(new_file_index)); try sema.addTypeReferenceEntry(src, ty); return .fromType(ty); @@ -13005,7 +13005,7 @@ fn zirImport(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air. const file = zcu.fileByIndex(file_index); switch (file.getMode()) { .zig => { - try pt.ensureFileAnalyzed(file_index); + try pt.ensureFilePopulated(file_index); const ty: Type = .fromInterned(zcu.fileRootType(file_index)); try sema.addTypeReferenceEntry(operand_src, ty); // No need for `ensureNamespaceUpToDate`, because `Zcu.PerThread.updateFileNamespace` @@ -34002,7 +34002,7 @@ pub fn analyzeMemoizedState(sema: *Sema, stage: InternPool.MemoizedStateStage) C // Get the main struct type of the root source file of `std`. No need for a reference entry // because `std` is always an analysis root. const std_file_index = zcu.module_roots.get(zcu.std_mod).?.unwrap().?; - try pt.ensureFileAnalyzed(std_file_index); + try pt.ensureFilePopulated(std_file_index); const std_type: Type = .fromInterned(zcu.fileRootType(std_file_index)); break :block .{ .parent = null, diff --git a/src/Zcu.zig b/src/Zcu.zig @@ -3208,6 +3208,8 @@ fn markTransitiveDependersPotentiallyOutdated(zcu: *Zcu, maybe_outdated: AnalUni /// recursive analysis (all of its previously-marked dependencies are already up-to-date), because /// recursive analysis can cause over-analysis on incremental updates. pub fn findOutdatedToAnalyze(zcu: *Zcu) Allocator.Error!?AnalUnit { + // MLUGG TODO: priorize `func` units, just like we used to do in the Compilation job queue. + if (zcu.outdated_ready.count() > 0) { const unit = zcu.outdated_ready.keys()[0]; log.debug("findOutdatedToAnalyze: {f}", .{zcu.fmtAnalUnit(unit)}); diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig @@ -27,7 +27,9 @@ const introspect = @import("../introspect.zig"); const Module = @import("../Package.zig").Module; const Sema = @import("../Sema.zig"); const target_util = @import("../target.zig"); -const trace = @import("../tracy.zig").trace; +const tracy = @import("../tracy.zig"); +const trace = tracy.trace; +const traceNamed = tracy.traceNamed; const Type = @import("../Type.zig"); const Value = @import("../Value.zig"); const Zcu = @import("../Zcu.zig"); @@ -125,6 +127,318 @@ pub fn deactivate(pt: Zcu.PerThread) void { pt.zcu.intern_pool.deactivate(); } +/// Called from `Compilation.performAllTheWork`. Performs one incremental update of the ZCU: detects +/// changes to files, runs AstGen, and then enters the main semantic analysis loop, where we build +/// up a graph of declarations, functions, etc, while also sending declarations and functions to +/// codegen as they are analyzed. +pub fn update( + pt: Zcu.PerThread, + main_progress_node: std.Progress.Node, + decl_work_timer: *?Compilation.Timer, +) (Allocator.Error || Io.Cancelable)!void { + const zcu = pt.zcu; + const comp = zcu.comp; + const gpa = comp.gpa; + const io = comp.io; + + { + const tracy_trace = traceNamed(@src(), "astgen"); + defer tracy_trace.end(); + + const zir_prog_node = main_progress_node.start("AST Lowering", 0); + defer zir_prog_node.end(); + + var timer = comp.startTimer(); + defer if (timer.finish(io)) |ns| { + comp.mutex.lockUncancelable(io); + defer comp.mutex.unlock(io); + comp.time_report.?.stats.real_ns_files = ns; + }; + + var astgen_group: Io.Group = .init; + defer astgen_group.cancel(io); + + // We cannot reference `zcu.import_table` after we spawn any `workerUpdateFile` jobs, + // because on single-threaded targets the worker will be run eagerly, meaning the + // `import_table` could be mutated, and not even holding `comp.mutex` will save us. So, + // build up a list of the files to update *before* we spawn any jobs. + var astgen_work_items: std.MultiArrayList(struct { + file_index: Zcu.File.Index, + file: *Zcu.File, + }) = .empty; + defer astgen_work_items.deinit(gpa); + // Not every item in `import_table` will need updating, because some are builtin.zig + // files. However, most will, so let's just reserve sufficient capacity upfront. + try astgen_work_items.ensureTotalCapacity(gpa, zcu.import_table.count()); + for (zcu.import_table.keys()) |file_index| { + const file = zcu.fileByIndex(file_index); + if (file.is_builtin) { + // This is a `builtin.zig`, so updating is redundant. However, we want to make + // sure the file contents are still correct on disk, since it can improve the + // debugging experience better. That job only needs `file`, so we can kick it + // off right now. + astgen_group.async(io, workerUpdateBuiltinFile, .{ comp, file }); + continue; + } + astgen_work_items.appendAssumeCapacity(.{ + .file_index = file_index, + .file = file, + }); + } + + // Now that we're not going to touch `zcu.import_table` again, we can spawn `workerUpdateFile` jobs. + for (astgen_work_items.items(.file_index), astgen_work_items.items(.file)) |file_index, file| { + astgen_group.async(io, workerUpdateFile, .{ + comp, file, file_index, zir_prog_node, &astgen_group, + }); + } + + // On the other hand, it's fine to directly iterate `zcu.embed_table.keys()` here + // because `workerUpdateEmbedFile` can't invalidate it. The different here is that one + // `@embedFile` can't trigger analysis of a new `@embedFile`! + for (0.., zcu.embed_table.keys()) |ef_index_usize, ef| { + const ef_index: Zcu.EmbedFile.Index = @enumFromInt(ef_index_usize); + astgen_group.async(io, workerUpdateEmbedFile, .{ + comp, ef_index, ef, + }); + } + + try astgen_group.await(io); + } + + // On an incremental update, a source file might become "dead", in that all imports of + // the file were removed. This could even change what module the file belongs to! As such, + // we do a traversal over the files, to figure out which ones are alive and the modules + // they belong to. + const any_fatal_files = try pt.computeAliveFiles(); + + // If the cache mode is `whole`, add every alive source file to the manifest. + switch (comp.cache_use) { + .whole => |whole| if (whole.cache_manifest) |man| { + for (zcu.alive_files.keys()) |file_index| { + const file = zcu.fileByIndex(file_index); + + switch (file.status) { + .never_loaded => unreachable, // AstGen tried to load it + .retryable_failure => continue, // the file cannot be read; this is a guaranteed error + .astgen_failure, .success => {}, // the file was read successfully + } + + const path = try file.path.toAbsolute(comp.dirs, gpa); + defer gpa.free(path); + + const result = res: { + try whole.cache_manifest_mutex.lock(io); + defer whole.cache_manifest_mutex.unlock(io); + if (file.source) |source| { + break :res man.addFilePostContents(path, source, file.stat); + } else { + break :res man.addFilePost(path); + } + }; + result catch |err| switch (err) { + error.OutOfMemory => |e| return e, + else => { + try pt.reportRetryableFileError(file_index, "unable to update cache: {s}", .{@errorName(err)}); + continue; + }, + }; + } + }, + .none, .incremental => {}, + } + + if (comp.time_report) |*tr| { + tr.stats.n_reachable_files = @intCast(zcu.alive_files.count()); + } + + if (any_fatal_files or + zcu.multi_module_err != null or + zcu.failed_imports.items.len > 0 or + comp.alloc_failure_occurred) + { + // We give up right now! No updating of ZIR refs, no nothing. The idea is that this prevents + // us from invalidating lots of incremental dependencies due to files with e.g. parse errors. + // However, this means our analysis data is invalid, so we want to omit all analysis errors. + zcu.skip_analysis_this_update = true; + return; + } + + if (comp.config.incremental) { + const update_zir_refs_node = main_progress_node.start("Update ZIR References", 0); + defer update_zir_refs_node.end(); + try pt.updateZirRefs(); + } + + try zcu.flushRetryableFailures(); + + if (!zcu.backendSupportsFeature(.separate_thread)) { + // Close the ZCU task queue. Prelink may still be running, but the closed + // queue will cause the linker task to exit once prelink finishes. The + // closed queue also communicates to `enqueueZcu` that it should wait for + // the linker task to finish and then run ZCU tasks serially. + comp.link_queue.finishZcuQueue(comp); + } + + zcu.sema_prog_node = main_progress_node.start("Semantic Analysis", 0); + if (comp.bin_file != null) { + zcu.codegen_prog_node = main_progress_node.start("Code Generation", 0); + } + // We increment `pending_codegen_jobs` so that it doesn't reach 0 until after analysis finishes. + // That prevents the "Code Generation" node from constantly disappearing and reappearing when + // we're probably going to analyze more functions at some point. + assert(zcu.pending_codegen_jobs.swap(1, .monotonic) == 0); // don't let this become 0 until analysis finishes + + defer { + zcu.sema_prog_node.end(); + zcu.sema_prog_node = .none; + if (zcu.pending_codegen_jobs.fetchSub(1, .monotonic) == 1) { + // Decremented to 0, so all done. + zcu.codegen_prog_node.end(); + zcu.codegen_prog_node = .none; + } + } + + // Start the timer for the "decls" part of the pipeline (Sema, CodeGen, link). + decl_work_timer.* = comp.startTimer(); + + // To kick off semantic analysis, populate the root source file of any module we have marked + // as an analysis root. Declarations in these files which want eager analysis---those being + // `comptime` declarations, any declarations marked `export`, and `test` declarations in the + // main module if this is a test compilation---become referenced, and so will be picked up + // up by the main semantic analysis loop below. + for (zcu.analysisRoots()) |analysis_root_mod| { + const analysis_root_file = zcu.module_roots.get(analysis_root_mod).?.unwrap().?; + try pt.ensureFilePopulated(analysis_root_file); + } + + // This is the main semantic analysis loop, which is essentially the main loop of the whole + // Zig compilation pipeline. It selects some `AnalUnit` which we know needs to be analyzed, + // and analyzes it, which may in turn discover more `AnalUnit`s which we need to analyze. + while (try zcu.findOutdatedToAnalyze()) |unit| { + const tracy_trace = traceNamed(@src(), "analyze_outdated"); + defer tracy_trace.end(); + + const maybe_err: Zcu.SemaError!void = switch (unit.unwrap()) { + .@"comptime" => |cu| pt.ensureComptimeUnitUpToDate(cu), + .nav_ty => |nav| pt.ensureNavTypeUpToDate(nav, null), + .nav_val => |nav| pt.ensureNavValUpToDate(nav, null), + .type_layout => |ty| pt.ensureTypeLayoutUpToDate(.fromInterned(ty), null), + .memoized_state => |stage| pt.ensureMemoizedStateUpToDate(stage, null), + .func => |func| pt.ensureFuncBodyUpToDate(func, null), + }; + maybe_err catch |err| switch (err) { + error.OutOfMemory, + error.Canceled, + => |e| return e, + + error.AnalysisFail => {}, // already reported + }; + } +} +fn workerUpdateBuiltinFile(comp: *Compilation, file: *Zcu.File) void { + Builtin.updateFileOnDisk(file, comp) catch |err| comp.lockAndSetMiscFailure( + .write_builtin_zig, + "unable to write '{f}': {s}", + .{ file.path.fmt(comp), @errorName(err) }, + ); +} +fn workerUpdateFile( + comp: *Compilation, + file: *Zcu.File, + file_index: Zcu.File.Index, + prog_node: std.Progress.Node, + group: *Io.Group, +) void { + const io = comp.io; + const tid: Zcu.PerThread.Id = .acquire(io); + defer tid.release(io); + + const child_prog_node = prog_node.start(std.fs.path.basename(file.path.sub_path), 0); + defer child_prog_node.end(); + + const pt: Zcu.PerThread = .activate(comp.zcu.?, tid); + defer pt.deactivate(); + pt.updateFile(file_index, file) catch |err| { + pt.reportRetryableFileError(file_index, "unable to load '{s}': {s}", .{ std.fs.path.basename(file.path.sub_path), @errorName(err) }) catch |oom| switch (oom) { + error.OutOfMemory => { + comp.mutex.lockUncancelable(io); + defer comp.mutex.unlock(io); + comp.setAllocFailure(); + }, + }; + return; + }; + + switch (file.getMode()) { + .zig => {}, // continue to logic below + .zon => return, // ZON can't import anything so we're done + } + + // Discover all imports in the file. Imports of modules we ignore for now since we don't + // know which module we're in, but imports of file paths might need us to queue up other + // AstGen jobs. + const imports_index = file.zir.?.extra[@intFromEnum(Zir.ExtraIndex.imports)]; + if (imports_index != 0) { + const extra = file.zir.?.extraData(Zir.Inst.Imports, imports_index); + var import_i: u32 = 0; + var extra_index = extra.end; + + while (import_i < extra.data.imports_len) : (import_i += 1) { + const item = file.zir.?.extraData(Zir.Inst.Imports.Item, extra_index); + extra_index = item.end; + + const import_path = file.zir.?.nullTerminatedString(item.data.name); + + if (pt.discoverImport(file.path, import_path)) |res| switch (res) { + .module, .existing_file => {}, + .new_file => |new| { + group.async(io, workerUpdateFile, .{ + comp, new.file, new.index, prog_node, group, + }); + }, + } else |err| switch (err) { + error.OutOfMemory => { + comp.mutex.lockUncancelable(io); + defer comp.mutex.unlock(io); + comp.setAllocFailure(); + }, + } + } + } +} +fn workerUpdateEmbedFile(comp: *Compilation, ef_index: Zcu.EmbedFile.Index, ef: *Zcu.EmbedFile) void { + const io = comp.io; + const tid: Zcu.PerThread.Id = .acquire(io); + defer tid.release(io); + detectEmbedFileUpdate(comp, tid, ef_index, ef) catch |err| switch (err) { + error.OutOfMemory => { + comp.mutex.lockUncancelable(io); + defer comp.mutex.unlock(io); + comp.setAllocFailure(); + }, + }; +} +fn detectEmbedFileUpdate(comp: *Compilation, tid: Zcu.PerThread.Id, ef_index: Zcu.EmbedFile.Index, ef: *Zcu.EmbedFile) !void { + const io = comp.io; + const zcu = comp.zcu.?; + const pt: Zcu.PerThread = .activate(zcu, tid); + defer pt.deactivate(); + + const old_val = ef.val; + const old_err = ef.err; + + try pt.updateEmbedFile(ef, null); + + if (ef.val != .none and ef.val == old_val) return; // success, value unchanged + if (ef.val == .none and old_val == .none and ef.err == old_err) return; // failure, error unchanged + + comp.mutex.lockUncancelable(io); + defer comp.mutex.unlock(io); + + try zcu.markDependeeOutdated(.not_marked_po, .{ .embed_file = ef_index }); +} + fn deinitFile(pt: Zcu.PerThread, file_index: Zcu.File.Index) void { const zcu = pt.zcu; const gpa = zcu.gpa; @@ -156,8 +470,8 @@ pub fn updateFile( ) !void { dev.check(.ast_gen); - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const comp = zcu.comp; @@ -484,7 +798,7 @@ fn cleanupUpdatedFiles(gpa: Allocator, updated_files: *std.AutoArrayHashMapUnman updated_files.deinit(gpa); } -pub fn updateZirRefs(pt: Zcu.PerThread) Allocator.Error!void { +fn updateZirRefs(pt: Zcu.PerThread) (Io.Cancelable || Allocator.Error)!void { assert(pt.tid == .main); const zcu = pt.zcu; const comp = zcu.comp; @@ -566,7 +880,7 @@ pub fn updateZirRefs(pt: Zcu.PerThread) Allocator.Error!void { const old_line = old_zir.getDeclaration(old_inst).src_line; const new_line = new_zir.getDeclaration(new_inst).src_line; if (old_line != new_line) { - try comp.queueJob(.{ .update_line_number = tracked_inst_index }); + try comp.link_queue.enqueueZcu(comp, pt.tid, .{ .debug_update_line_number = tracked_inst_index }); } }, else => {}, @@ -674,11 +988,11 @@ pub fn updateZirRefs(pt: Zcu.PerThread) Allocator.Error!void { /// Typical Zig compilations begin by claling this function on the root source file of the standard /// library, `lib/std/std.zig`. The resulting namespace scan discovers a `comptime` declaration in /// that file, which is queued for analysis, and everything goes from there. -pub fn ensureFileAnalyzed(pt: Zcu.PerThread, file_index: Zcu.File.Index) (Allocator.Error || Io.Cancelable)!void { +pub fn ensureFilePopulated(pt: Zcu.PerThread, file_index: Zcu.File.Index) (Allocator.Error || Io.Cancelable)!void { dev.check(.sema); - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const comp = zcu.comp; @@ -734,8 +1048,8 @@ pub fn ensureMemoizedStateUpToDate( /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, ) Zcu.SemaError!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const gpa = zcu.gpa; @@ -844,8 +1158,8 @@ fn analyzeMemoizedState( /// if necessary. Returns `error.AnalysisFail` if an analysis error is encountered; the caller is /// free to ignore this, since the error is already registered. pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeUnit.Id) Zcu.SemaError!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1008,8 +1322,8 @@ pub fn ensureTypeLayoutUpToDate( /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, ) Zcu.SemaError!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const comp = zcu.comp; @@ -1121,8 +1435,8 @@ pub fn ensureNavValUpToDate( /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, ) Zcu.SemaError!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1457,12 +1771,20 @@ fn analyzeNavVal( if (!queue_linker_work) break :queue_codegen; if (!nav_ty.hasRuntimeBits(zcu)) { - if (zcu.comp.config.use_llvm) break :queue_codegen; + if (comp.config.use_llvm) break :queue_codegen; if (file.mod.?.strip) break :queue_codegen; } - zcu.comp.link_prog_node.increaseEstimatedTotalItems(1); - try zcu.comp.queueJob(.{ .link_nav = nav_id }); + comp.link_prog_node.increaseEstimatedTotalItems(1); + try comp.link_queue.enqueueZcu(comp, pt.tid, .{ .link_nav = nav_id }); + } + + if (comp.config.is_test and zcu.test_functions.contains(nav_id)) { + // We just analyzed a test function's "value" (essentially its signature); now we need to + // implicitly reference the function *body*. `Zcu.resolveReferences` knows about this rule, + // so we don't need to mark an explicit reference, but we do need to make sure that the test + // body will actually get analyzed! + try zcu.ensureFuncBodyAnalysisQueued(nav_val.toIntern()); } switch (old_nav.status) { @@ -1477,8 +1799,8 @@ pub fn ensureNavTypeUpToDate( /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, ) Zcu.SemaError!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1719,8 +2041,8 @@ pub fn ensureFuncBodyUpToDate( ) Zcu.SemaError!void { dev.check(.sema); - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1846,7 +2168,8 @@ fn analyzeFuncBody( log.debug("analyze and generate fn body {f}", .{zcu.fmtAnalUnit(anal_unit)}); var air = try pt.analyzeFuncBodyInner(func_index, reason); - errdefer air.deinit(gpa); + var air_owned = true; + errdefer if (air_owned) air.deinit(gpa); const ies_outdated = !func.analysisUnordered(ip).inferred_error_set or func.resolvedErrorSetUnordered(ip) != old_resolved_ies; @@ -1856,17 +2179,22 @@ fn analyzeFuncBody( const dump_air = build_options.enable_debug_extensions and comp.verbose_air; const dump_llvm_ir = build_options.enable_debug_extensions and (comp.verbose_llvm_ir != null or comp.verbose_llvm_bc != null); - if (comp.bin_file == null and zcu.llvm_object == null and !dump_air and !dump_llvm_ir) { - air.deinit(gpa); - return .{ .ies_outdated = ies_outdated }; - } + if (comp.bin_file != null or zcu.llvm_object != null or dump_air or dump_llvm_ir) { + zcu.codegen_prog_node.increaseEstimatedTotalItems(1); + comp.link_prog_node.increaseEstimatedTotalItems(1); - zcu.codegen_prog_node.increaseEstimatedTotalItems(1); - comp.link_prog_node.increaseEstimatedTotalItems(1); - try comp.queueJob(.{ .codegen_func = .{ - .func = func_index, - .air = air, - } }); + // Some linkers need to refer to the AIR. In that case, the linker is not running + // concurrently, so we'll just keep ownership of the AIR for ourselves instead of + // letting the codegen job destroy it. + const disown_air = zcu.backendSupportsFeature(.separate_thread); + + // Begin the codegen task. If the codegen/link queue is backed up, this might + // block until the linker is able to process some tasks. + const codegen_task = try zcu.codegen_task_pool.start(zcu, func_index, &air, disown_air); + if (disown_air) air_owned = false; + + try comp.link_queue.enqueueZcu(comp, pt.tid, .{ .link_func = codegen_task }); + } return .{ .ies_outdated = ies_outdated }; } @@ -2121,7 +2449,7 @@ pub fn populateModuleRootTable(pt: Zcu.PerThread) error{ /// modify `pt.zcu.skip_analysis_this_update`. /// /// If an error is returned, `pt.zcu.alive_files` might contain undefined values. -pub fn computeAliveFiles(pt: Zcu.PerThread) Allocator.Error!bool { +fn computeAliveFiles(pt: Zcu.PerThread) Allocator.Error!bool { const zcu = pt.zcu; const comp = zcu.comp; const gpa = zcu.gpa; @@ -2562,8 +2890,8 @@ pub fn scanNamespace( namespace_index: Zcu.Namespace.Index, decls: []const Zir.Inst.Index, ) Allocator.Error!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const ip = &zcu.intern_pool; @@ -2659,8 +2987,8 @@ const ScanDeclIter = struct { } fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const pt = iter.pt; const zcu = pt.zcu; @@ -2713,77 +3041,65 @@ const ScanDeclIter = struct { const existing_unit = iter.existing_by_inst.get(tracked_inst); - const unit: AnalUnit, const want_analysis = switch (decl.kind) { - .@"comptime" => unit: { - const cu = if (existing_unit) |eu| - eu.unwrap().@"comptime" - else - try ip.createComptimeUnit(gpa, io, pt.tid, tracked_inst, namespace_index); - + const name = maybe_name.unwrap() orelse { + // Only `comptime` declarations are unnamed. + assert(decl.kind == .@"comptime"); + if (existing_unit) |unit| { + try namespace.comptime_decls.append(gpa, unit.unwrap().@"comptime"); + } else { + const cu = try ip.createComptimeUnit(gpa, io, pt.tid, tracked_inst, namespace_index); + try zcu.queueComptimeUnitAnalysis(cu); try namespace.comptime_decls.append(gpa, cu); + } + return; + }; - if (existing_unit == null) { - // For a `comptime` declaration, whether to analyze is based solely on whether the unit - // is outdated. So, add this fresh one to `outdated` and `outdated_ready`. - try zcu.queueComptimeUnitAnalysis(cu); - } + const fqn = try namespace.internFullyQualifiedName(ip, gpa, io, pt.tid, name); + + const nav = if (existing_unit) |unit| nav: { + const nav = unit.unwrap().nav_val; + assert(ip.getNav(nav).name == name); + assert(ip.getNav(nav).fqn == fqn); + break :nav nav; + } else nav: { + const nav = try ip.createDeclNav(gpa, io, pt.tid, name, fqn, tracked_inst, namespace_index); + if (zcu.comp.debugIncremental()) try zcu.incremental_debug_state.newNav(zcu, nav); + break :nav nav; + }; - break :unit .{ .wrap(.{ .@"comptime" = cu }), true }; + const want_analysis: bool = switch (decl.kind) { + .@"comptime" => unreachable, + .unnamed_test, .@"test", .decltest => a: { + const is_named = decl.kind != .unnamed_test; + try namespace.test_decls.append(gpa, nav); + // TODO: incremental compilation! + // * remove from `test_functions` if no longer matching filter + // * add to `test_functions` if newly passing filter + // This logic is unaware of incremental: we'll end up with duplicates. + // Perhaps we should add all test indiscriminately and filter at the end of the update. + if (!comp.config.is_test) break :a false; + if (file.mod != zcu.main_mod) break :a false; + if (is_named and comp.test_filters.len > 0) { + const fqn_slice = fqn.toSlice(ip); + for (comp.test_filters) |test_filter| { + if (std.mem.indexOf(u8, fqn_slice, test_filter) != null) break; + } else break :a false; + } + try zcu.test_functions.put(gpa, nav, {}); + break :a true; }, - else => unit: { - const name = maybe_name.unwrap().?; - const fqn = try namespace.internFullyQualifiedName(ip, gpa, io, pt.tid, name); - const nav = if (existing_unit) |eu| eu.unwrap().nav_val else nav: { - const nav = try ip.createDeclNav(gpa, io, pt.tid, name, fqn, tracked_inst, namespace_index); - if (zcu.comp.debugIncremental()) try zcu.incremental_debug_state.newNav(zcu, nav); - break :nav nav; - }; - - const unit: AnalUnit = .wrap(.{ .nav_val = nav }); - - assert(ip.getNav(nav).name == name); - assert(ip.getNav(nav).fqn == fqn); - - const want_analysis = switch (decl.kind) { - .@"comptime" => unreachable, - .unnamed_test, .@"test", .decltest => a: { - const is_named = decl.kind != .unnamed_test; - try namespace.test_decls.append(gpa, nav); - // TODO: incremental compilation! - // * remove from `test_functions` if no longer matching filter - // * add to `test_functions` if newly passing filter - // This logic is unaware of incremental: we'll end up with duplicates. - // Perhaps we should add all test indiscriminately and filter at the end of the update. - if (!comp.config.is_test) break :a false; - if (file.mod != zcu.main_mod) break :a false; - if (is_named and comp.test_filters.len > 0) { - const fqn_slice = fqn.toSlice(ip); - for (comp.test_filters) |test_filter| { - if (std.mem.indexOf(u8, fqn_slice, test_filter) != null) break; - } else break :a false; - } - try zcu.test_functions.put(gpa, nav, {}); - break :a true; - }, - .@"const", .@"var" => a: { - if (decl.is_pub) { - try namespace.pub_decls.putContext(gpa, nav, {}, .{ .zcu = zcu }); - } else { - try namespace.priv_decls.putContext(gpa, nav, {}, .{ .zcu = zcu }); - } - break :a false; - }, - }; - break :unit .{ unit, want_analysis }; + .@"const", .@"var" => a: { + if (decl.is_pub) { + try namespace.pub_decls.putContext(gpa, nav, {}, .{ .zcu = zcu }); + } else { + try namespace.priv_decls.putContext(gpa, nav, {}, .{ .zcu = zcu }); + } + break :a false; }, }; - if (existing_unit == null and (want_analysis or decl.linkage == .@"export")) { - log.debug( - "scanDecl queue analyze_unit file='{s}' unit={f}", - .{ namespace.fileScope(zcu).sub_file_path, zcu.fmtAnalUnit(unit) }, - ); - try comp.queueJob(.{ .analyze_unit = unit }); + if (want_analysis or decl.linkage == .@"export") { + try zcu.ensureNavValAnalysisQueued(nav); } } }; @@ -2793,8 +3109,8 @@ fn analyzeFuncBodyInner( func_index: InternPool.Index, reason: ?*const Zcu.DependencyReason, ) Zcu.SemaError!Air { - const tracy = trace(@src()); - defer tracy.end(); + const tracy_trace = trace(@src()); + defer tracy_trace.end(); const zcu = pt.zcu; const comp = zcu.comp; @@ -3437,36 +3753,45 @@ pub fn intern(pt: Zcu.PerThread, key: InternPool.Key) Allocator.Error!InternPool /// Essentially a shortcut for calling `intern_pool.getCoerced`. /// However, this function also allows coercing `extern`s. The `InternPool` function can't do -/// this because it requires potentially pushing to the job queue. +/// this because it requires potentially queueing a link task. pub fn getCoerced(pt: Zcu.PerThread, val: Value, new_ty: Type) Allocator.Error!Value { const ip = &pt.zcu.intern_pool; const comp = pt.zcu.comp; const gpa = comp.gpa; const io = comp.io; switch (ip.indexToKey(val.toIntern())) { - .@"extern" => |e| { - const coerced = try pt.getExtern(.{ - .name = e.name, + .@"extern" => |@"extern"| { + // TODO: it's awkward to make this function cancelable. The problem is really that + // `getCoerced` is a bad API: it should be replaced with smaller, more specialized + // functions, so that this cancel point is only possible in the rare case that you + // may actually need to coerce an extern! + const old_prot = io.swapCancelProtection(.blocked); + defer _ = io.swapCancelProtection(old_prot); + const coerced = pt.getExtern(.{ + .name = @"extern".name, .ty = new_ty.toIntern(), - .lib_name = e.lib_name, - .is_const = e.is_const, - .is_threadlocal = e.is_threadlocal, - .linkage = e.linkage, - .visibility = e.visibility, - .is_dll_import = e.is_dll_import, - .relocation = e.relocation, - .decoration = e.decoration, - .alignment = e.alignment, - .@"addrspace" = e.@"addrspace", - .zir_index = e.zir_index, + .lib_name = @"extern".lib_name, + .is_const = @"extern".is_const, + .is_threadlocal = @"extern".is_threadlocal, + .linkage = @"extern".linkage, + .visibility = @"extern".visibility, + .is_dll_import = @"extern".is_dll_import, + .relocation = @"extern".relocation, + .decoration = @"extern".decoration, + .alignment = @"extern".alignment, + .@"addrspace" = @"extern".@"addrspace", + .zir_index = @"extern".zir_index, .owner_nav = undefined, // ignored by `getExtern`. - .source = e.source, - }); - return Value.fromInterned(coerced); + .source = @"extern".source, + }) catch |err| switch (err) { + error.Canceled => unreachable, // blocked above + error.OutOfMemory => |e| return e, + }; + return .fromInterned(coerced); }, else => {}, } - return Value.fromInterned(try ip.getCoerced(gpa, io, pt.tid, val.toIntern(), new_ty.toIntern())); + return .fromInterned(try ip.getCoerced(gpa, io, pt.tid, val.toIntern(), new_ty.toIntern())); } pub fn intType(pt: Zcu.PerThread, signedness: std.builtin.Signedness, bits: u16) Allocator.Error!Type { @@ -3865,14 +4190,15 @@ pub fn navPtrType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Allocator.Err /// Intern an `.@"extern"`, creating a corresponding owner `Nav` if necessary. /// If necessary, the new `Nav` is queued for codegen. /// `key.owner_nav` is ignored and may be `undefined`. -pub fn getExtern(pt: Zcu.PerThread, key: InternPool.Key.Extern) Allocator.Error!InternPool.Index { +pub fn getExtern(pt: Zcu.PerThread, key: InternPool.Key.Extern) (Io.Cancelable || Allocator.Error)!InternPool.Index { const zcu = pt.zcu; const comp = zcu.comp; + Type.fromInterned(key.ty).assertHasLayout(zcu); const result = try zcu.intern_pool.getExtern(comp.gpa, comp.io, pt.tid, key); if (result.new_nav.unwrap()) |nav| { - comp.link_prog_node.increaseEstimatedTotalItems(1); - try comp.queueJob(.{ .link_nav = nav }); if (comp.debugIncremental()) try zcu.incremental_debug_state.newNav(zcu, nav); + comp.link_prog_node.increaseEstimatedTotalItems(1); + try comp.link_queue.enqueueZcu(comp, pt.tid, .{ .link_nav = nav }); } return result.index; }