From f134f4345cf8484b82c46278074eb45af0efaf2e Mon Sep 17 00:00:00 2001 From: Matthew Lugg Date: Fri, 31 Jul 2026 13:39:16 +0100 Subject: [PATCH] compiler: improve tracking of transitive analysis errors This is an internal refactor to mark transitive semantic analysis errors as soon as they occur rather than relying on the root "update" function to do so. This is simpler to understand and slightly more efficient. The error surfaced in this case is renamed from `error.AnalysisFail` to `error.AlreadyReported` for consistency with the rest of the compiler. `error.AnalysisFail` is returned from the "ensure up to date" functions in `Zcu.PerThread` to indicate that the unit which was requested has failed analysis---using a different error name here is useful because it prevents `Sema` from accidentally introducing a bug by `try`ing. Alongside the above, I have also begun to store some useful debugging information (the reason for the transitive analysis error) with transitive analysis errors in compilers built with debug extensions. This information is surfaced by the incremental debug server, and was invaluable in tracking down an incremental compilation bug---details of that in the next commit. --- src/Compilation.zig | 9 +- src/IncrementalDebugServer.zig | 27 +++-- src/Sema.zig | 185 ++++++++++++++++++++++++--------- src/Sema/LowerZon.zig | 4 +- src/Sema/type_resolution.zig | 28 +++-- src/Zcu.zig | 21 +++- src/Zcu/PerThread.zig | 163 +++++++++++++---------------- 7 files changed, 275 insertions(+), 162 deletions(-) diff --git a/src/Compilation.zig b/src/Compilation.zig index 6561f88312664801ea4dc1d217ecf85ae413c1af..0952e9f7dc05456c4a8d4e6cbc35fa17d14ef484 100644 --- a/src/Compilation.zig +++ b/src/Compilation.zig @@ -4076,7 +4076,14 @@ pub fn getAllErrorsAlloc(comp: *Compilation) error{OutOfMemory}!ErrorBundle { ref = refs.get(r.referencer).?; } } - @panic("referenced transitive analysis errors, but none actually emitted"); + if (comp.debugIncremental()) { + std.debug.print("skipping compiler panic to allow incremental debug server usage", .{}); + try bundle.addRootErrorMessage(.{ + .msg = try bundle.addString("compiler bug: referenced transitive analysis errors, but none actually emitted"), + }); + } else { + @panic("referenced transitive analysis errors, but none actually emitted"); + } } }; diff --git a/src/IncrementalDebugServer.zig b/src/IncrementalDebugServer.zig index 4d34812ae507191ea3799f12209d482008e14b93..20c1af1969ad117ee63acd6428f1ee0c8490f6e6 100644 --- a/src/IncrementalDebugServer.zig +++ b/src/IncrementalDebugServer.zig @@ -286,21 +286,34 @@ fn handleCommand(zcu: *Zcu, w: *Io.Writer, cmd_str: []const u8, arg_str: []const const referencer = (ref orelse break :ref "").referencer; break :ref printAnalUnit(referencer, &ref_str_buf); }; - const has_err: []const u8 = err: { - if (zcu.failed_analysis.contains(unit)) break :err "true"; - if (zcu.transitive_failed_analysis.contains(unit)) break :err "true (transitive)"; - break :err "false"; - }; try w.print( \\last update generation: {d} \\current referencer: {s} - \\has error: {s} \\ , .{ unit_info.last_update_gen, ref_str, - has_err, }); + if (zcu.failed_analysis.get(unit)) |err_msg| { + try w.print("analysis result: failure ({q})\n", .{err_msg.msg}); + } else if (zcu.transitive_failed_analysis.get(unit)) |reason| { + switch (reason) { + .astgen_error => try w.writeAll("analysis result: transitive failure (astgen error)\n"), + .dependency_loop => try w.writeAll("analysis result: transitive failure (dependency loop)\n"), + .lost_tracking => try w.writeAll("analysis result: transitive failure (lost tracking for zir inst)\n"), + .failed_unit => |other_unit| { + var buf: [32]u8 = undefined; + try w.print("analysis result: transitive failure (failed unit: {s})\n", .{printAnalUnit(other_unit, &buf)}); + }, + .func_nav_val_changed => |func_index| try w.print("analysis result: transitive failure (owner nav of func '{d}' changed value)\n", .{@backingInt(func_index)}), + } + } else { + try w.writeAll("analysis result: success\n"); + } + if (unit.unwrap() == .func) { + const nav_id = zcu.intern_pool.indexToKey(unit.unwrap().func).func.owner_nav; + try w.print("owner nav: {d}\n", .{@backingInt(nav_id)}); + } } else if (std.mem.eql(u8, cmd_str, "unit_dependencies")) { const unit = parseAnalUnit(arg_str) orelse return w.writeAll("malformed anal unit"); const unit_info = zcu.incremental_debug_state.units.get(unit) orelse return w.writeAll("unknown anal unit"); diff --git a/src/Sema.zig b/src/Sema.zig index 2181fa563b9e9dec42b1123cc3b738874df64ca7..09268824c6dd2063f6a94d836416a36375c11e42 100644 --- a/src/Sema.zig +++ b/src/Sema.zig @@ -1472,7 +1472,7 @@ fn analyzeBodyInner( i += 1; continue; }, - .astgen_error => return error.AnalysisFail, + .astgen_error => return sema.failTransitive(.astgen_error), .float_op_result_ty => try sema.zirFloatOpResultType(block, extended), }; }, @@ -2697,13 +2697,15 @@ fn failWithTypeMismatch(sema: *Sema, block: *Block, src: LazySrcLoc, expected: T }); } -pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg) error{ AnalysisFail, OutOfMemory } { +pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg) SemaError { @branchHint(.cold); const zcu = sema.pt.zcu; const comp = zcu.comp; const gpa = comp.gpa; const io = comp.io; + assert(sema.err == null); + if (build_options.enable_debug_extensions and comp.debug_compile_errors) { var wip_errors: std.zig.ErrorBundle.Wip = undefined; wip_errors.init(gpa) catch @panic("out of memory"); @@ -2729,17 +2731,11 @@ pub fn failWithOwnedErrorMsg(sema: *Sema, block: ?*Block, err_msg: *Zcu.ErrorMsg err_msg.reference_trace_root = sema.owner.toOptional(); - const gop = try zcu.failed_analysis.getOrPut(gpa, sema.owner); - if (gop.found_existing) { - // If there are multiple errors for the same Decl, prefer the first one added. - sema.err = null; - err_msg.destroy(gpa); - } else { - sema.err = err_msg; - gop.value_ptr.* = err_msg; - } + try zcu.failed_analysis.putNoClobber(gpa, sema.owner, err_msg); + assert(!zcu.transitive_failed_analysis.contains(sema.owner)); - return error.AnalysisFail; + sema.err = err_msg; + return error.AlreadyReported; } /// Given an ErrorMsg, modify its message and source location to the given values, turning the @@ -4745,11 +4741,14 @@ fn failWithBadMemberAccess( .@"enum" => "enum", else => unreachable, }; - if (agg_ty.typeDeclInst(zcu)) |inst| if ((inst.resolve(ip) orelse return error.AnalysisFail) == .main_struct_inst) { - return sema.fail(block, field_src, "root source file struct '{f}' has no member named '{f}'", .{ - agg_ty.fmt(pt), field_name.fmt(ip), - }); - }; + if (agg_ty.typeDeclInst(zcu)) |inst| { + const inst_index = inst.resolve(ip) orelse return sema.failTransitive(.{ .lost_tracking = inst }); + if (inst_index == .main_struct_inst) { + return sema.fail(block, field_src, "root source file struct '{f}' has no member named '{f}'", .{ + agg_ty.fmt(pt), field_name.fmt(ip), + }); + } + } return sema.fail(block, field_src, "{s} '{f}' has no member named '{f}'", .{ kw_name, agg_ty.fmt(pt), field_name.fmt(ip), @@ -5997,7 +5996,14 @@ fn lookupInNamespace( const pt = sema.pt; const zcu = pt.zcu; - try pt.ensureNamespaceUpToDate(namespace_index); + pt.ensureNamespaceUpToDate(namespace_index) catch |err| switch (err) { + error.LostZirContainerDecl => { + const namespace = zcu.namespacePtr(namespace_index); + const ns_ty: Type = .fromInterned(namespace.owner_type); + return sema.failTransitive(.{ .lost_tracking = ns_ty.typeDeclInstAllowGeneratedTag(zcu).? }); + }, + else => |e| return e, + }; const namespace = zcu.namespacePtr(namespace_index); @@ -6062,7 +6068,7 @@ pub fn analyzeSaveErrRetIndex(sema: *Sema, block: *Block) SemaError!Air.Inst.Ref const stack_trace_ty = try sema.getStdLangType(block.nodeOffset(.zero), .StackTrace); const field_name = try zcu.intern_pool.getOrPutString(gpa, io, pt.tid, "index", .no_embedded_nulls); const field_index = sema.structFieldIndex(block, stack_trace_ty, field_name, LazySrcLoc.unneeded) catch |err| switch (err) { - error.AnalysisFail => @panic("std.lang.StackTrace is corrupt"), + error.AlreadyReported => @panic("std.lang.StackTrace is corrupt"), error.ComptimeReturn, error.ComptimeBreak => unreachable, error.OutOfMemory, error.Canceled => |e| return e, }; @@ -6724,7 +6730,9 @@ fn analyzeCall( const fn_nav: InternPool.Nav, const fn_zir: Zir, const fn_tracked_inst: InternPool.TrackedInst.Index, const fn_zir_inst: Zir.Inst.Index, const fn_zir_info: Zir.FnInfo = if (func_val) |f| b: { const info = ip.indexToKey(f.toIntern()).func; const nav = ip.getNav(info.owner_nav); - const resolved_func_inst = info.zir_body_inst.resolveFull(ip) orelse return error.AnalysisFail; + const resolved_func_inst = info.zir_body_inst.resolveFull(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = info.zir_body_inst }); + }; const file = zcu.fileByIndex(resolved_func_inst.file); const zir_info = file.zir.?.getFnInfo(resolved_func_inst.inst); break :b .{ nav, file.zir.?, info.zir_body_inst, resolved_func_inst.inst, zir_info }; @@ -8355,7 +8363,10 @@ fn zirFunc( const cc: std.lang.CallingConvention = if (has_body) cc: { const func_decl_nav = sema.owner.unwrap().nav_val; const fn_is_exported = exported: { - const decl_inst = ip.getNav(func_decl_nav).analysis.?.zir_index.resolve(ip) orelse return error.AnalysisFail; + const decl_ti = ip.getNav(func_decl_nav).analysis.?.zir_index; + const decl_inst = decl_ti.resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = decl_ti }); + }; const zir_decl = sema.code.getDeclaration(decl_inst); break :exported zir_decl.linkage == .@"export"; }; @@ -12289,10 +12300,11 @@ fn analyzeSwitchPayloadCaptureTaggedUnion( dummy_captures, .{ .override = item_srcs }, ) catch |err| switch (err) { - error.AnalysisFail => { - const msg = sema.err orelse return error.AnalysisFail; - try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); - return error.AnalysisFail; + error.AlreadyReported => |e| { + if (sema.err) |msg| { + try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); + } + return e; }, else => |e| return e, }; @@ -12328,11 +12340,12 @@ fn analyzeSwitchPayloadCaptureTaggedUnion( dummy_captures, .{ .override = item_srcs }, ) catch |err| switch (err) { - error.AnalysisFail => { - const msg = sema.err orelse return error.AnalysisFail; - try sema.errNote(capture_src, msg, "this coercion is only possible when capturing by value", .{}); - try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); - return error.AnalysisFail; + error.AlreadyReported => |e| { + if (sema.err) |msg| { + try sema.errNote(capture_src, msg, "this coercion is only possible when capturing by value", .{}); + try sema.reparentOwnedErrorMsg(capture_src, msg, "capture group with incompatible types", .{}); + } + return e; }, else => |e| return e, }; @@ -17207,7 +17220,14 @@ fn typeInfoNamespaceDecls( const ip = &zcu.intern_pool; const namespace_index = opt_namespace_index.unwrap() orelse return; - try pt.ensureNamespaceUpToDate(namespace_index); + pt.ensureNamespaceUpToDate(namespace_index) catch |err| switch (err) { + error.LostZirContainerDecl => { + const namespace = zcu.namespacePtr(namespace_index); + const ns_ty: Type = .fromInterned(namespace.owner_type); + return sema.failTransitive(.{ .lost_tracking = ns_ty.typeDeclInstAllowGeneratedTag(zcu).? }); + }, + else => |e| return e, + }; const namespace = zcu.namespacePtr(namespace_index); const gop = try seen_namespaces.getOrPut(namespace); @@ -17933,11 +17953,12 @@ fn zirUnreachable(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError } // TODO Add compile error for @optimizeFor occurring too late in a scope. sema.analyzeUnreachable(block, src, true) catch |err| switch (err) { - error.AnalysisFail => { - const msg = sema.err orelse return err; - if (!mem.eql(u8, msg.msg, "runtime safety check not allowed in naked function")) return err; - try sema.errNote(src, msg, "the end of a naked function is implicitly unreachable", .{}); - return err; + error.AlreadyReported => |e| { + if (sema.err) |msg| { + if (!mem.eql(u8, msg.msg, "runtime safety check not allowed in naked function")) return err; + try sema.errNote(src, msg, "the end of a naked function is implicitly unreachable", .{}); + } + return e; }, else => |e| return e, }; @@ -18353,11 +18374,16 @@ fn zirPtrType(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!Air const elem_ty = blk: { const air_inst = sema.resolveInst(extra.data.elem_type); - const ty = sema.analyzeAsType(block, elem_ty_src, .type, air_inst) catch |err| { - if (err == error.AnalysisFail and sema.err != null and sema.typeOf(air_inst).isSinglePointer(zcu)) { - try sema.errNote(elem_ty_src, sema.err.?, "use '.*' to dereference pointer", .{}); - } - return err; + const ty = sema.analyzeAsType(block, elem_ty_src, .type, air_inst) catch |err| switch (err) { + error.AlreadyReported => |e| { + if (sema.err) |msg| { + if (sema.typeOf(air_inst).isSinglePointer(zcu)) { + try sema.errNote(elem_ty_src, msg, "use '.*' to dereference pointer", .{}); + } + } + return e; + }, + else => |e| return e, }; assert(!ty.isGenericPoison()); break :blk ty; @@ -24909,7 +24935,10 @@ fn zirFuncFancy(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A } else cc: { if (has_body) { const func_decl_nav = sema.owner.unwrap().nav_val; - const func_decl_inst = ip.getNav(func_decl_nav).analysis.?.zir_index.resolve(&zcu.intern_pool) orelse return error.AnalysisFail; + const func_decl_ti = ip.getNav(func_decl_nav).analysis.?.zir_index; + const func_decl_inst = func_decl_ti.resolve(&zcu.intern_pool) orelse { + return sema.failTransitive(.{ .lost_tracking = func_decl_ti }); + }; const zir_decl = sema.code.getDeclaration(func_decl_inst); if (zir_decl.linkage == .@"export") { break :cc target.cCallingConvention() orelse { @@ -30642,7 +30671,10 @@ fn ensureMemoizedStateResolved(sema: *Sema, src: LazySrcLoc, stage: InternPool.M if (pt.zcu.analysis_in_progress.contains(unit)) { return sema.failWithDependencyLoop(unit, &reason); } - try pt.ensureMemoizedStateUpToDate(stage, &reason); + pt.ensureMemoizedStateUpToDate(stage, &reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = unit }), + else => |e| return e, + }; } pub fn ensureNavResolved(sema: *Sema, block: *Block, src: LazySrcLoc, nav_index: InternPool.Nav.Index, kind: enum { type, fully }) CompileError!void { @@ -30678,9 +30710,15 @@ pub fn ensureNavResolved(sema: *Sema, block: *Block, src: LazySrcLoc, nav_index: switch (kind) { .type => { try zcu.ensureNavValAnalysisQueued(nav_index); - return pt.ensureNavTypeUpToDate(nav_index, &reason); + return pt.ensureNavTypeUpToDate(nav_index, &reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = anal_unit }), + else => |e| return e, + }; + }, + .fully => return pt.ensureNavValUpToDate(nav_index, &reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = anal_unit }), + else => |e| return e, }, - .fully => return pt.ensureNavValUpToDate(nav_index, &reason), } } @@ -33743,7 +33781,10 @@ fn ensureFuncIesResolved( return sema.failWithDependencyLoop(.wrap(.{ .func = func_index }), &reason); } - try pt.ensureFuncBodyUpToDate(func_index, &reason); + pt.ensureFuncBodyUpToDate(func_index, &reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = .wrap(.{ .func = func_index }) }), + else => |e| return e, + }; } pub fn resolveInferredErrorSetPtr( @@ -34962,7 +35003,9 @@ pub fn setTypeName( }, .parent => wip.setName(ip, block.type_name_ctx, sema.owner.unwrap().nav_val.toOptional()), .func => { - const fn_info = sema.code.getFnInfo(ip.funcZirBodyInst(sema.func_index).resolve(ip) orelse return error.AnalysisFail); + const fn_info = sema.code.getFnInfo(ip.funcZirBodyInst(sema.func_index).resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = ip.funcZirBodyInst(sema.func_index) }); + }); const zir_tags = sema.code.instructions.items(.tag); var aw: std.Io.Writer.Allocating = .init(gpa); @@ -35078,7 +35121,10 @@ fn zirStructDecl( }; try sema.addTypeReferenceEntry(src, ty); - try pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)); + pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)) catch |err| switch (err) { + error.LostZirContainerDecl => unreachable, // we literally just tracked it + else => |e| return e, + }; return .fromType(ty); } @@ -35151,7 +35197,10 @@ fn zirUnionDecl( }; try sema.addTypeReferenceEntry(src, ty); - try pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)); + pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)) catch |err| switch (err) { + error.LostZirContainerDecl => unreachable, // we literally just tracked it + else => |e| return e, + }; return .fromType(ty); } @@ -35203,7 +35252,10 @@ fn zirEnumDecl( }; try sema.addTypeReferenceEntry(src, ty); - try pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)); + pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)) catch |err| switch (err) { + error.LostZirContainerDecl => unreachable, // we literally just tracked it + else => |e| return e, + }; return .fromType(ty); } @@ -35252,7 +35304,10 @@ fn zirOpaqueDecl( }; try sema.addTypeReferenceEntry(src, ty); - try pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)); + pt.ensureNamespaceUpToDate(ty.getNamespaceIndex(zcu)) catch |err| switch (err) { + error.LostZirContainerDecl => unreachable, // we literally just tracked it + else => |e| return e, + }; return .fromType(ty); } @@ -35293,5 +35348,31 @@ pub fn failWithDependencyLoop( } // A dependency loop error will be reported. Mark us all as transitive failures. - return error.AnalysisFail; + return sema.failTransitive(.dependency_loop); +} + +/// Marks the owner of `sema` as having failed semantic failed *without* an error message, and +/// returns failure. This function is suitable to call when any one of the following is true: +/// +/// * `sema.owner` is guaranteed to be unreferenced on this update, for instance because it uses a +/// dead `InternPool.TrackedInst`. +/// +/// * There is guaranteed to be a compile error if this unit is referenced. In practice, this means +/// that either there is an error elsewhere in the pipeline (e.g. AstGen), or we depend on another +/// `AnalUnit` which has itself failed. +pub fn failTransitive(sema: *Sema, reason: Zcu.TransitiveFailureReason) SemaError { + assert(sema.err == null); + const zcu = sema.pt.zcu; + const unit = sema.owner; + + log.debug("transitive failure analyzing '{f}' ({t})", .{ zcu.fmtAnalUnit(unit), reason }); + + assert(!zcu.failed_analysis.contains(unit)); + try zcu.transitive_failed_analysis.putNoClobber( + zcu.comp.gpa, + unit, + if (build_options.enable_debug_extensions) reason, + ); + + return error.AlreadyReported; } diff --git a/src/Sema/LowerZon.zig b/src/Sema/LowerZon.zig index f807043d8ecd7bbf6c628feb877d25c92212d08c..dd8c91c244518ac23d1d5d0be6caa30861f2c4c7 100644 --- a/src/Sema/LowerZon.zig +++ b/src/Sema/LowerZon.zig @@ -320,7 +320,7 @@ fn failUnsupportedResultType( self: *LowerZon, ty: Type, opt_note: ?[]const u8, -) error{ AnalysisFail, OutOfMemory } { +) Zcu.SemaError { @branchHint(.cold); const sema = self.sema; const gpa = sema.gpa; @@ -338,7 +338,7 @@ fn fail( node: Zoir.Node.Index, comptime format: []const u8, args: anytype, -) error{ AnalysisFail, OutOfMemory } { +) Zcu.SemaError { @branchHint(.cold); const err_msg = try Zcu.ErrorMsg.create(self.sema.pt.zcu.gpa, self.nodeSrc(node), format, args); try self.sema.pt.zcu.errNote(self.import_loc, err_msg, "imported here", .{}); diff --git a/src/Sema/type_resolution.zig b/src/Sema/type_resolution.zig index b3de4f8433a768a6a5c4c174fb300f1a899edf64..1e089955e59e2efcbd3b40185cab8289460c956f 100644 --- a/src/Sema/type_resolution.zig +++ b/src/Sema/type_resolution.zig @@ -116,7 +116,10 @@ fn ensureLayoutResolvedInner(sema: *Sema, ty: Type, orig_ty: Type, reason: *cons if (zcu.analysis_in_progress.contains(.wrap(.{ .type_layout = ty.toIntern() }))) { return sema.failWithDependencyLoop(.wrap(.{ .type_layout = ty.toIntern() }), reason); } - try pt.ensureTypeLayoutUpToDate(ty, reason); + pt.ensureTypeLayoutUpToDate(ty, reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = .wrap(.{ .type_layout = ty.toIntern() }) }), + else => |e| return e, + }; }, // values, not types @@ -166,7 +169,10 @@ pub fn ensureStructDefaultsResolved(sema: *Sema, ty: Type, src: LazySrcLoc) Sema return sema.failWithDependencyLoop(.wrap(.{ .struct_defaults = ty.toIntern() }), &reason); } - try pt.ensureStructDefaultsUpToDate(ty, &reason); + pt.ensureStructDefaultsUpToDate(ty, &reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = .wrap(.{ .struct_defaults = ty.toIntern() }) }), + else => |e| return e, + }; } /// Asserts that `struct_ty` is a non-packed non-tuple struct, and that `sema.owner` is that type. @@ -188,7 +194,9 @@ pub fn resolveStructLayout(sema: *Sema, struct_ty: Type) CompileError!void { const struct_obj = ip.loadStructType(struct_ty.toIntern()); assert(struct_obj.want_layout); - const zir_index = struct_obj.zir_index.resolve(ip) orelse return error.AnalysisFail; + const zir_index = struct_obj.zir_index.resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = struct_obj.zir_index }); + }; var block: Block = .{ .parent = null, @@ -606,7 +614,7 @@ pub fn resolveStructDefaults(sema: *Sema, struct_ty: Type) CompileError!void { struct_ty.assertHasLayout(zcu); const layout_unit: InternPool.AnalUnit = .wrap(.{ .type_layout = struct_ty.toIntern() }); if (zcu.failed_analysis.contains(layout_unit) or zcu.transitive_failed_analysis.contains(layout_unit)) { - return error.AnalysisFail; + return sema.failTransitive(.{ .failed_unit = layout_unit }); } const struct_obj = ip.loadStructType(struct_ty.toIntern()); @@ -656,7 +664,9 @@ fn resolveStructDefaultsInner( assert(struct_obj.field_defaults.len > 0); // We'll need to map the struct decl instruction to provide result types - const zir_index = struct_obj.zir_index.resolve(ip) orelse return error.AnalysisFail; + const zir_index = struct_obj.zir_index.resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = struct_obj.zir_index }); + }; try sema.inst_map.ensureSpaceForInstructions(gpa, &.{zir_index}); const field_types = struct_obj.field_types.get(ip); @@ -713,7 +723,9 @@ pub fn resolveUnionLayout(sema: *Sema, union_ty: Type) CompileError!void { const union_obj = ip.loadUnionType(union_ty.toIntern()); assert(union_obj.want_layout); - const zir_index = union_obj.zir_index.resolve(ip) orelse return error.AnalysisFail; + const zir_index = union_obj.zir_index.resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = union_obj.zir_index }); + }; var block: Block = .{ .parent = null, @@ -1212,7 +1224,9 @@ pub fn resolveEnumLayout(sema: *Sema, enum_ty: Type) CompileError!void { }; const tracked_inst = enum_obj.zir_index.unwrap() orelse maybe_parent_union_obj.?.zir_index; - const zir_index = tracked_inst.resolve(ip) orelse return error.AnalysisFail; + const zir_index = tracked_inst.resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = tracked_inst }); + }; var block: Block = .{ .parent = null, diff --git a/src/Zcu.zig b/src/Zcu.zig index 9ed2ed7dc708d3939a1baf479fdd11798415e6fb..465506eb7b41dcbcb16620c7ef84dc8b5f406ea4 100644 --- a/src/Zcu.zig +++ b/src/Zcu.zig @@ -182,7 +182,10 @@ analysis_in_progress: std.array_hash_map.Auto(AnalUnit, ?*const DependencyReason /// The ErrorMsg memory is owned by the `AnalUnit`, using Module's general purpose allocator. failed_analysis: std.array_hash_map.Auto(AnalUnit, *ErrorMsg) = .empty, /// This `AnalUnit` failed semantic analysis because it required analysis of another `AnalUnit` which itself failed. -transitive_failed_analysis: std.array_hash_map.Auto(AnalUnit, void) = .empty, +transitive_failed_analysis: std.array_hash_map.Auto( + AnalUnit, + if (build_options.enable_debug_extensions) TransitiveFailureReason else void, +) = .empty, /// This `Nav` succeeded analysis, but failed codegen. /// This may be a simple "value" `Nav`, or it may be a function. /// The ErrorMsg memory is owned by the `AnalUnit`, using Module's general purpose allocator. @@ -351,6 +354,18 @@ pub const DependencyReason = struct { type_layout_reason: Sema.type_resolution.LayoutResolveReason, }; +/// These are not required for anything, but when the compiler is built with debug extensions, we +/// store these in `Zcu.transitive_failed_analysis` and surface them in the incremental debug server +/// (see `src/IncrementalDebugServer.zig`) because they are a useful debugging aid for bugs in +/// incremental compilation. +pub const TransitiveFailureReason = union(enum) { + astgen_error, + dependency_loop, + lost_tracking: InternPool.TrackedInst.Index, + failed_unit: AnalUnit, + func_nav_val_changed: InternPool.Index, +}; + pub const IncrementalDebugState = struct { /// All container types in the ZCU, even dead ones. /// Value is the generation the type was created on. @@ -2808,13 +2823,13 @@ pub const LazySrcLoc = struct { } }; -pub const SemaError = error{ OutOfMemory, Canceled, AnalysisFail }; +pub const SemaError = error{ OutOfMemory, Canceled, AlreadyReported }; pub const CompileError = error{ OutOfMemory, /// The compilation update is no longer desired. Canceled, /// When this is returned, the compile error for the failure has already been recorded. - AnalysisFail, + AlreadyReported, /// In a comptime scope, a return instruction was encountered. This error is only seen when /// doing a comptime function call. ComptimeReturn, diff --git a/src/Zcu/PerThread.zig b/src/Zcu/PerThread.zig index b1292e6dedb38753832091e60c5535343dfef0c2..d062f68ec946d85481b9a29c5c2eb5de5cf6e5ca 100644 --- a/src/Zcu/PerThread.zig +++ b/src/Zcu/PerThread.zig @@ -320,7 +320,7 @@ pub fn update( // 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 maybe_err: Zcu.SemaError!void = switch (unit.unwrap()) { + const maybe_err: UpdateUnitError!void = switch (unit.unwrap()) { .@"comptime" => |cu| pt.ensureComptimeUnitUpToDate(cu), .nav_ty => |nav| pt.ensureNavTypeUpToDate(nav, null), .nav_val => |nav| pt.ensureNavValUpToDate(nav, null), @@ -332,7 +332,7 @@ pub fn update( error.Canceled, => |e| return e, - error.AnalysisFail => {}, // already reported + error.AnalysisFail => {}, }; break :res pt.ensureStructDefaultsUpToDate(.fromInterned(ty), null); }, @@ -344,7 +344,7 @@ pub fn update( error.Canceled, => |e| return e, - error.AnalysisFail => {}, // already reported + error.AnalysisFail => {}, }; } } @@ -455,7 +455,7 @@ fn detectEmbedFileUpdate(comp: *Compilation, tid: Zcu.PerThread.Id, ef_index: Zc /// Ensures that `file` has up-to-date ZIR. If not, loads the ZIR cache or runs /// AstGen as needed. Also updates `file.status`. Does not assume that `file.mod` -/// is populated. Does not return `error.AnalysisFail` on AstGen failures. +/// is populated. Returns success even if the file has AstGen errors. pub fn updateFile( pt: Zcu.PerThread, file_index: Zcu.File.Index, @@ -1036,6 +1036,11 @@ pub fn ensureFilePopulated(pt: Zcu.PerThread, file_index: Zcu.File.Index) (Alloc zcu.setFileRootType(file_index, wip.finish(ip, new_namespace_index)); } +const UpdateUnitError = Allocator.Error || Io.Cancelable || error{ + /// Semantic analysis of this `AnalUnit` failed. + AnalysisFail, +}; + /// Ensures that all memoized state on `Zcu` is up-to-date, performing re-analysis if necessary. /// Returns `error.AnalysisFail` if an analysis error is encountered; the caller is free to ignore /// this, since the error is already registered, but it must not use the value of memoized fields. @@ -1044,7 +1049,7 @@ pub fn ensureMemoizedStateUpToDate( stage: InternPool.MemoizedStateStage, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1078,15 +1083,7 @@ pub fn ensureMemoizedStateUpToDate( const any_changed: bool, const new_failed: bool = if (pt.analyzeMemoizedState(stage, reason)) |any_changed| .{ any_changed or prev_failed, false } else |err| switch (err) { - error.AnalysisFail => res: { - if (!zcu.failed_analysis.contains(unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(unit)}); - } - break :res .{ !prev_failed, true }; - }, + error.AlreadyReported => .{ !prev_failed, true }, error.OutOfMemory => { // TODO: same as for `ensureComptimeUnitUpToDate` etc return error.OutOfMemory; @@ -1154,7 +1151,7 @@ fn analyzeMemoizedState( /// Ensures that the state of the given `ComptimeUnit` is fully up-to-date, performing re-analysis /// 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 { +pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeUnit.Id) UpdateUnitError!void { const zcu = pt.zcu; const gpa = zcu.gpa; @@ -1195,15 +1192,7 @@ pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeU defer unit_tracking.end(zcu); return pt.analyzeComptimeUnit(cu_id) catch |err| switch (err) { - error.AnalysisFail => { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - return error.AnalysisFail; - }, + error.AlreadyReported => return error.AnalysisFail, error.OutOfMemory => { // TODO: it's unclear how to gracefully handle this. // To report the error cleanly, we need to add a message to `failed_analysis` and a @@ -1221,8 +1210,7 @@ pub fn ensureComptimeUnitUpToDate(pt: Zcu.PerThread, cu_id: InternPool.ComptimeU /// Re-analyzes a `ComptimeUnit`. The unit has already been determined to be out-of-date, and old /// side effects (exports/references/etc) have been dropped. If semantic analysis fails, this -/// function will return `error.AnalysisFail`, and it is the caller's reponsibility to add an entry -/// to `transitive_failed_analysis` if necessary. +/// function will return `error.AlreadyReported`. fn analyzeComptimeUnit(pt: Zcu.PerThread, cu_id: InternPool.ComptimeUnit.Id) Zcu.CompileError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; @@ -1239,7 +1227,14 @@ fn analyzeComptimeUnit(pt: Zcu.PerThread, cu_id: InternPool.ComptimeUnit.Id) Zcu defer tracy_trace.end(); tracy_trace.addTextFmt("cu_id={d}", .{cu_id}); - const inst_resolved = comptime_unit.zir_index.resolveFull(ip) orelse return error.AnalysisFail; + const inst_resolved = comptime_unit.zir_index.resolveFull(ip) orelse { + try zcu.transitive_failed_analysis.putNoClobber( + gpa, + anal_unit, + if (build_options.enable_debug_extensions) .{ .lost_tracking = comptime_unit.zir_index }, + ); + return error.AlreadyReported; + }; const file = zcu.fileByIndex(inst_resolved.file); const zir = file.zir.?; @@ -1314,7 +1309,7 @@ pub fn ensureTypeLayoutUpToDate( ty: Type, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; const comp = zcu.comp; @@ -1399,15 +1394,7 @@ pub fn ensureTypeLayoutUpToDate( const new_failed: bool = if (result) failed: { break :failed false; } else |err| switch (err) { - error.AnalysisFail => failed: { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - break :failed true; - }, + error.AlreadyReported => true, error.OutOfMemory, error.Canceled, => |e| return e, @@ -1442,7 +1429,7 @@ pub fn ensureStructDefaultsUpToDate( ty: Type, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; const comp = zcu.comp; @@ -1513,15 +1500,7 @@ pub fn ensureStructDefaultsUpToDate( const new_failed: bool = if (Sema.type_resolution.resolveStructDefaults(&sema, ty)) failed: { break :failed false; } else |err| switch (err) { - error.AnalysisFail => failed: { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - break :failed true; - }, + error.AlreadyReported => true, error.OutOfMemory, error.Canceled, => |e| return e, @@ -1547,7 +1526,7 @@ pub fn ensureNavValUpToDate( nav_id: InternPool.Nav.Index, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { const zcu = pt.zcu; const gpa = zcu.gpa; const ip = &zcu.intern_pool; @@ -1594,15 +1573,7 @@ pub fn ensureNavValUpToDate( false, }; } else |err| switch (err) { - error.AnalysisFail => res: { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - break :res .{ !prev_failed, true }; - }, + error.AlreadyReported => .{ !prev_failed, true }, error.OutOfMemory => { // TODO: it's unclear how to gracefully handle this. // To report the error cleanly, we need to add a message to `failed_analysis` and a @@ -1655,7 +1626,14 @@ fn analyzeNavVal( tracy_trace.addText(old_nav.fqn.toSlice(ip)); tracy_trace.addTextFmt("nav_id={d}", .{nav_id}); - const inst_resolved = old_nav.analysis.?.zir_index.resolveFull(ip) orelse return error.AnalysisFail; + const inst_resolved = old_nav.analysis.?.zir_index.resolveFull(ip) orelse { + try zcu.transitive_failed_analysis.putNoClobber( + gpa, + anal_unit, + if (build_options.enable_debug_extensions) .{ .lost_tracking = old_nav.analysis.?.zir_index }, + ); + return error.AlreadyReported; + }; const file = zcu.fileByIndex(inst_resolved.file); const zir = file.zir.?; const zir_decl = zir.getDeclaration(inst_resolved.inst); @@ -1916,7 +1894,7 @@ pub fn ensureNavTypeUpToDate( nav_id: InternPool.Nav.Index, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { const zcu = pt.zcu; const gpa = zcu.gpa; const ip = &zcu.intern_pool; @@ -1963,15 +1941,7 @@ pub fn ensureNavTypeUpToDate( false, }; } else |err| switch (err) { - error.AnalysisFail => res: { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this unit caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - break :res .{ !prev_failed, true }; - }, + error.AlreadyReported => .{ !prev_failed, true }, error.OutOfMemory => { // TODO: it's unclear how to gracefully handle this. // To report the error cleanly, we need to add a message to `failed_analysis` and a @@ -2024,7 +1994,14 @@ fn analyzeNavType( tracy_trace.addText(old_nav.fqn.toSlice(ip)); tracy_trace.addTextFmt("nav_id={d}", .{nav_id}); - const inst_resolved = old_nav.analysis.?.zir_index.resolveFull(ip) orelse return error.AnalysisFail; + const inst_resolved = old_nav.analysis.?.zir_index.resolveFull(ip) orelse { + try zcu.transitive_failed_analysis.putNoClobber( + gpa, + anal_unit, + if (build_options.enable_debug_extensions) .{ .lost_tracking = old_nav.analysis.?.zir_index }, + ); + return error.AlreadyReported; + }; const file = zcu.fileByIndex(inst_resolved.file); const zir = file.zir.?; @@ -2160,7 +2137,7 @@ pub fn ensureFuncBodyUpToDate( func_index: InternPool.Index, /// `null` is valid only for the "root" analysis, i.e. called from `Compilation.processOneJob`. reason: ?*const Zcu.DependencyReason, -) Zcu.SemaError!void { +) UpdateUnitError!void { dev.check(.sema); const zcu = pt.zcu; @@ -2204,18 +2181,10 @@ pub fn ensureFuncBodyUpToDate( const ies_outdated, const new_failed = if (pt.analyzeFuncBody(func_index, reason)) |result| .{ prev_failed or result.ies_outdated, false } else |err| switch (err) { - error.AnalysisFail => res: { - if (!zcu.failed_analysis.contains(anal_unit)) { - // If this function caused the error, it would have an entry in `failed_analysis`. - // Since it does not, this must be a transitive failure. - try zcu.transitive_failed_analysis.put(gpa, anal_unit, {}); - log.debug("mark transitive analysis failure for {f}", .{zcu.fmtAnalUnit(anal_unit)}); - } - // We consider the IES to be outdated if the function previously succeeded analysis; in this case, - // we need to re-analyze dependants to ensure they hit a transitive error here, rather than reporting - // a different error later (which may now be invalid). - break :res .{ !prev_failed, true }; - }, + // We consider the IES to be outdated if the function previously succeeded analysis; in this case, + // we need to re-analyze dependants to ensure they hit a transitive error here, rather than reporting + // a different error later (which may now be invalid). + error.AlreadyReported => .{ !prev_failed, true }, error.OutOfMemory => { // TODO: it's unclear how to gracefully handle this. // To report the error cleanly, we need to add a message to `failed_analysis` and a @@ -3306,15 +3275,21 @@ fn analyzeFuncBodyInner( // If we *are* still owned by the right NAV, this analysis updates `zir_body_inst` if necessary. if (func.generic_owner == .none) { - try pt.ensureNavValUpToDate(func.owner_nav, reason); + pt.ensureNavValUpToDate(func.owner_nav, reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = .wrap(.{ .nav_val = func.owner_nav }) }), + else => |e| return e, + }; if (ip.getNav(func.owner_nav).resolved.?.value != func_index) { - return error.AnalysisFail; + return sema.failTransitive(.{ .func_nav_val_changed = func_index }); } } else { const go_nav = zcu.funcInfo(func.generic_owner).owner_nav; - try pt.ensureNavValUpToDate(go_nav, reason); + pt.ensureNavValUpToDate(go_nav, reason) catch |err| switch (err) { + error.AnalysisFail => return sema.failTransitive(.{ .failed_unit = .wrap(.{ .nav_val = go_nav }) }), + else => |e| return e, + }; if (ip.getNav(go_nav).resolved.?.value != func.generic_owner) { - return error.AnalysisFail; + return sema.failTransitive(.{ .func_nav_val_changed = func.generic_owner }); } } @@ -3344,7 +3319,9 @@ fn analyzeFuncBodyInner( }; defer inner_block.instructions.deinit(gpa); - const fn_info = sema.code.getFnInfo(func.zirBodyInstUnordered(ip).resolve(ip) orelse return error.AnalysisFail); + const fn_info = sema.code.getFnInfo(func.zirBodyInstUnordered(ip).resolve(ip) orelse { + return sema.failTransitive(.{ .lost_tracking = func.zirBodyInstUnordered(ip) }); + }); // Here we are performing "runtime semantic analysis" for a function body, which means // we must map the parameter ZIR instructions to `arg` AIR instructions. @@ -4378,12 +4355,18 @@ pub fn getExtern(pt: Zcu.PerThread, key: InternPool.Key.Extern) (Io.Cancelable | return result.index; } +const UpdateNamespaceError = Allocator.Error || Io.Cancelable || error{ + /// This namespace refers to a ZIR container declaration which no longer exists, so any code + /// referencing it is guaranteed to be unreferenced on this update. + LostZirContainerDecl, +}; + /// Given a namespace, re-scan its declarations from the type definition if they have not /// yet been re-scanned on this update. -/// If the type declaration instruction has been lost, returns `error.AnalysisFail`. +/// If the type declaration instruction has been lost, returns `error.LostZirContainerDecl`. /// This will effectively short-circuit the caller, which will be semantic analysis of a /// guaranteed-unreferenced `AnalUnit`, to trigger a transitive analysis error. -pub fn ensureNamespaceUpToDate(pt: Zcu.PerThread, namespace_index: Zcu.Namespace.Index) Zcu.SemaError!void { +pub fn ensureNamespaceUpToDate(pt: Zcu.PerThread, namespace_index: Zcu.Namespace.Index) UpdateNamespaceError!void { const zcu = pt.zcu; const ip = &zcu.intern_pool; const namespace = zcu.namespacePtr(namespace_index); @@ -4410,7 +4393,7 @@ pub fn ensureNamespaceUpToDate(pt: Zcu.PerThread, namespace_index: Zcu.Namespace // Namespace outdated -- re-scan the type if necessary. - const inst_info = key.zir_index.resolveFull(ip) orelse return error.AnalysisFail; + const inst_info = key.zir_index.resolveFull(ip) orelse return error.LostZirContainerDecl; const file = zcu.fileByIndex(inst_info.file); const zir = &file.zir.?; -- 2.54.0