From 42fedb3f6c8f9c54da8faea35f424cbd3f72c386 Mon Sep 17 00:00:00 2001 From: mlugg Date: Fri, 8 Mar 2024 12:49:15 +0000 Subject: Zcu: handle incremental updates (more) correctly when scanning namespaces --- src/Module.zig | 135 +++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 83 insertions(+), 52 deletions(-) (limited to 'src/Module.zig') diff --git a/src/Module.zig b/src/Module.zig index fef01a1c2c..257dc31d2d 100644 --- a/src/Module.zig +++ b/src/Module.zig @@ -4076,17 +4076,34 @@ pub fn scanNamespace( const gpa = zcu.gpa; const namespace = zcu.namespacePtr(namespace_index); + // For incremental updates, `scanDecl` wants to look up existing decls by their ZIR index rather + // than their name. We'll build an efficient mapping now, then discard the current `decls`. + var existing_by_inst: std.AutoHashMapUnmanaged(InternPool.TrackedInst.Index, Decl.Index) = .{}; + defer existing_by_inst.deinit(gpa); + + try existing_by_inst.ensureTotalCapacity(namespace.decls.count()); + + for (namespace.decls.keys()) |decl_index| { + const decl = zcu.declPtr(decl_index); + existing_by_inst.putAssumeCapacityNoClobber(decl.zir_decl_index.unwrap().?, decl_index); + } + var seen_decls: std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, void) = .{}; defer seen_decls.deinit(gpa); try zcu.comp.work_queue.ensureUnusedCapacity(decls.len); + + namespace.decls.clearRetainingCapacity(); try namespace.decls.ensureTotalCapacity(gpa, decls.len); + namespace.usingnamespace_set.clearRetainingCapacity(); + var scan_decl_iter: ScanDeclIter = .{ .zcu = zcu, .namespace_index = namespace_index, .parent_decl = parent_decl, .seen_decls = &seen_decls, + .existing_by_inst = &existing_by_inst, .pass = .named, }; for (decls) |decl_inst| { @@ -4118,6 +4135,7 @@ const ScanDeclIter = struct { namespace_index: Namespace.Index, parent_decl: *Decl, seen_decls: *std.AutoHashMapUnmanaged(InternPool.NullTerminatedString, void), + existing_by_inst: *const std.AutoHashMapUnmanaged(InternPool.TrackedInst.Index, Decl.Index), /// Decl scanning is run in two passes, so that we can detect when a generated /// name would clash with an explicit name and use a different one. pass: enum { named, unnamed }, @@ -4222,72 +4240,85 @@ fn scanDecl(iter: *ScanDeclIter, decl_inst: Zir.Inst.Index) Allocator.Error!void }, }; - if (kind == .@"usingnamespace") try namespace.usingnamespace_set.ensureUnusedCapacity(gpa, 1); + switch (kind) { + .@"usingnamespace" => try namespace.usingnamespace_set.ensureUnusedCapacity(gpa, 1), + .@"test" => try zcu.test_functions.ensureUnusedCapacity(gpa, 1), + else => {}, + } const tracked_inst = try ip.trackZir(gpa, iter.parent_decl.getFileScope(zcu), decl_inst); // We create a Decl for it regardless of analysis status. - const gop = try namespace.decls.getOrPutContextAdapted( - gpa, - decl_name, - DeclAdapter{ .zcu = zcu }, - Namespace.DeclContext{ .zcu = zcu }, - ); - const comp = zcu.comp; - if (!gop.found_existing) { + + const prev_exported, const decl_index = if (iter.existing_by_inst.get(tracked_inst)) |decl_index| decl_index: { + // We need only update this existing Decl. + const decl = zcu.declPtr(decl_index); + const was_exported = decl.is_exported; + assert(decl.kind == kind); // ZIR tracking should preserve this + assert(decl.alive); + decl.name = decl_name; + decl.src_node = decl_node; + decl.src_line = line; + decl.is_pub = declaration.flags.is_pub; + decl.is_exported = declaration.flags.is_export; + break :decl_index .{ was_exported, decl_index }; + } else decl_index: { + // Create and set up a new Decl. const new_decl_index = try zcu.allocateNewDecl(namespace_index, decl_node); const new_decl = zcu.declPtr(new_decl_index); new_decl.kind = kind; new_decl.name = decl_name; - if (kind == .@"usingnamespace") { - namespace.usingnamespace_set.putAssumeCapacity(new_decl_index, declaration.flags.is_pub); - } new_decl.src_line = line; - gop.key_ptr.* = new_decl_index; - // Exported decls, comptime decls, usingnamespace decls, and - // test decls if in test mode, get analyzed. - const decl_mod = namespace.file_scope.mod; - const want_analysis = declaration.flags.is_export or switch (kind) { - .anon => unreachable, - .@"comptime", .@"usingnamespace" => true, - .named => false, - .@"test" => a: { - if (!comp.config.is_test) break :a false; - if (decl_mod != zcu.main_mod) break :a false; - if (is_named_test and comp.test_filters.len > 0) { - const decl_fqn = ip.stringToSlice(try namespace.fullyQualifiedName(zcu, decl_name)); - for (comp.test_filters) |test_filter| { - if (mem.indexOf(u8, decl_fqn, test_filter)) |_| break; - } else break :a false; - } - try zcu.test_functions.put(gpa, new_decl_index, {}); - break :a true; - }, - }; - if (want_analysis) { - log.debug("scanDecl queue analyze_decl file='{s}' decl_name='{s}' decl_index={d}", .{ - namespace.file_scope.sub_file_path, ip.stringToSlice(decl_name), new_decl_index, - }); - comp.work_queue.writeItemAssumeCapacity(.{ .analyze_decl = new_decl_index }); - } new_decl.is_pub = declaration.flags.is_pub; new_decl.is_exported = declaration.flags.is_export; new_decl.zir_decl_index = tracked_inst.toOptional(); - new_decl.alive = true; // This Decl corresponds to an AST node and therefore always alive. - return; - } - const decl_index = gop.key_ptr.*; + new_decl.alive = true; // This Decl corresponds to an AST node and is therefore always alive. + break :decl_index .{ false, new_decl_index }; + }; + const decl = zcu.declPtr(decl_index); - // Update the AST node of the decl; even if its contents are unchanged, it may - // have been re-ordered. - decl.src_node = decl_node; - decl.src_line = line; - - decl.is_pub = declaration.flags.is_pub; - decl.is_exported = declaration.flags.is_export; - decl.kind = kind; - decl.zir_decl_index = tracked_inst.toOptional(); + + namespace.decls.putAssumeCapacityNoClobberContext(decl_index, {}, .{ .zcu = zcu }); + + const comp = zcu.comp; + const decl_mod = namespace.file_scope.mod; + const want_analysis = declaration.flags.is_export or switch (kind) { + .anon => unreachable, + .@"comptime" => true, + .@"usingnamespace" => a: { + namespace.usingnamespace_set.putNoClobber(decl_index, declaration.flags.is_pub); + break :a true; + }, + .named => false, + .@"test" => a: { + if (!comp.config.is_test) break :a false; + if (decl_mod != zcu.main_mod) break :a false; + if (is_named_test and comp.test_filters.len > 0) { + const decl_fqn = ip.stringToSlice(try namespace.fullyQualifiedName(zcu, decl_name)); + for (comp.test_filters) |test_filter| { + if (mem.indexOf(u8, decl_fqn, test_filter)) |_| break; + } else break :a false; + } + zcu.test_functions.putAssumeCapacity(decl_index, {}); // may clobber on incremental update + break :a true; + }, + }; + + if (want_analysis) { + // We will not queue analysis if the decl has been analyzed on a previous update and + // `is_export` is unchanged. In this case, the incremental update mechanism will handle + // re-analysis for us if necessary. + if (prev_exported != declaration.flags.is_export or decl.analysis == .unreferenced) { + log.debug("scanDecl queue analyze_decl file='{s}' decl_name='{s}' decl_index={d}", .{ + namespace.file_scope.sub_file_path, ip.stringToSlice(decl_name), decl_index, + }); + comp.work_queue.writeItemAssumeCapacity(.{ .analyze_decl = decl_index }); + } + } + if (decl.getOwnedFunction(zcu) != null) { + // TODO this logic is insufficient; namespaces we don't re-scan may still require + // updated line numbers. Look into this! // TODO Look into detecting when this would be unnecessary by storing enough state // in `Decl` to notice that the line number did not change. comp.work_queue.writeItemAssumeCapacity(.{ .update_line_number = decl_index }); -- cgit v1.2.3