authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-14 11:10:46+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2025-09-15 11:29:54+01:00
log4cfb58342ab40adc1a179bfde9fcb116f47bb9d7
tree07dc857d4d9656ef97f10fa1a89ae8b440f00918
parent965b2ab6c370c5bbb4e1ea2ba660125f889f572c
signaturelock-open Commit is signed but in an unrecognized format.

frontend: fix reference tracking through coerced function bodies

This bug was manifesting for user as a nasty link error because they were calling their application's main entry point as a coerced function, which essentially broke reference tracking for the entire ZCU, causing exported symbols to silently not get exported. I've been a little unsure about how coerced functions should interact with the unit graph before, but the solution is actually really obvious now: they shouldn't! `Sema` is now responsible for unwrapping possibly-coerced functions *before* queuing analysis or marking unit references. This makes the reference graph optimal (there are no redundant edges representing coerced versions of the same function) and simplifies logic elsewhere at the expense of just a few lines in Sema.

4 files changed, 64 insertions(+), 24 deletions(-)

src/Sema.zig+37-19
......@@ -4375,8 +4375,9 @@ fn zirResolveInferredAlloc(sema: *Sema, block: *Block, inst: Zir.Inst.Index) Com
43754375 if (zcu.intern_pool.isFuncBody(val)) {
43764376 const ty: Type = .fromInterned(zcu.intern_pool.typeOf(val));
43774377 if (try ty.fnHasRuntimeBitsSema(pt)) {
4378 try sema.addReferenceEntry(block, src, AnalUnit.wrap(.{ .func = val }));
4379 try zcu.ensureFuncBodyAnalysisQueued(val);
4378 const orig_fn_index = zcu.intern_pool.unwrapCoercedFunc(val);
4379 try sema.addReferenceEntry(block, src, .wrap(.{ .func = orig_fn_index }));
4380 try zcu.ensureFuncBodyAnalysisQueued(orig_fn_index);
43804381 }
43814382 }
43824383
......@@ -5588,16 +5589,21 @@ fn zirPanic(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void
55885589 }
55895590
55905591 try sema.ensureMemoizedStateResolved(src, .panic);
5591 try zcu.ensureFuncBodyAnalysisQueued(zcu.builtin_decl_values.get(.@"panic.call"));
5592
5593 const panic_fn = Air.internedToRef(zcu.builtin_decl_values.get(.@"panic.call"));
5594
5592 const panic_fn_index = zcu.builtin_decl_values.get(.@"panic.call");
55955593 const opt_usize_ty = try pt.optionalType(.usize_type);
55965594 const null_ret_addr = Air.internedToRef((try pt.intern(.{ .opt = .{
55975595 .ty = opt_usize_ty.toIntern(),
55985596 .val = .none,
55995597 } })));
5600 try sema.callBuiltin(block, src, panic_fn, .auto, &.{ coerced_msg, null_ret_addr }, .@"@panic");
5598 // `callBuiltin` also calls `addReferenceEntry` to the function body for us.
5599 try sema.callBuiltin(
5600 block,
5601 src,
5602 .fromIntern(panic_fn_index),
5603 .auto,
5604 &.{ coerced_msg, null_ret_addr },
5605 .@"@panic",
5606 );
56015607}
56025608
56035609fn zirTrap(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!void {
......@@ -7566,8 +7572,9 @@ fn analyzeCall(
75667572 ref_func: {
75677573 const runtime_func_val = try sema.resolveValue(runtime_func) orelse break :ref_func;
75687574 if (!ip.isFuncBody(runtime_func_val.toIntern())) break :ref_func;
7569 try sema.addReferenceEntry(block, call_src, .wrap(.{ .func = runtime_func_val.toIntern() }));
7570 try zcu.ensureFuncBodyAnalysisQueued(runtime_func_val.toIntern());
7575 const orig_fn_index = ip.unwrapCoercedFunc(runtime_func_val.toIntern());
7576 try sema.addReferenceEntry(block, call_src, .wrap(.{ .func = orig_fn_index }));
7577 try zcu.ensureFuncBodyAnalysisQueued(orig_fn_index);
75717578 }
75727579
75737580 const call_tag: Air.Inst.Tag = switch (modifier) {
......@@ -26360,23 +26367,27 @@ fn explainWhyTypeIsNotPacked(
2636026367/// instructions. This function ensures the panic function will be available to
2636126368/// be called during that time.
2636226369fn preparePanicId(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !void {
26370 const zcu = sema.pt.zcu;
26371
2636326372 // If the backend doesn't support `.panic_fn`, it doesn't want us to lower the panic handlers.
2636426373 // The backend will transform panics into traps instead.
26365 if (sema.pt.zcu.backendSupportsFeature(.panic_fn)) {
26366 _ = try sema.getPanicIdFunc(src, panic_id);
26367 }
26374 if (!zcu.backendSupportsFeature(.panic_fn)) return;
26375
26376 const fn_index = try sema.getPanicIdFunc(src, panic_id);
26377 const orig_fn_index = zcu.intern_pool.unwrapCoercedFunc(fn_index);
26378 try sema.addReferenceEntry(null, src, .wrap(.{ .func = orig_fn_index }));
26379 try zcu.ensureFuncBodyAnalysisQueued(orig_fn_index);
2636826380}
2636926381
2637026382fn getPanicIdFunc(sema: *Sema, src: LazySrcLoc, panic_id: Zcu.SimplePanicId) !InternPool.Index {
2637126383 const zcu = sema.pt.zcu;
2637226384 try sema.ensureMemoizedStateResolved(src, .panic);
26373 const panic_func = zcu.builtin_decl_values.get(panic_id.toBuiltin());
26374 try zcu.ensureFuncBodyAnalysisQueued(panic_func);
26385 const panic_fn_index = zcu.builtin_decl_values.get(panic_id.toBuiltin());
2637526386 switch (sema.owner.unwrap()) {
2637626387 .@"comptime", .nav_ty, .nav_val, .type, .memoized_state => {},
2637726388 .func => |owner_func| zcu.intern_pool.funcSetHasErrorTrace(owner_func, true),
2637826389 }
26379 return panic_func;
26390 return panic_fn_index;
2638026391}
2638126392
2638226393fn addSafetyCheck(
......@@ -31164,6 +31175,11 @@ fn addReferenceEntry(
3116431175 referenced_unit: AnalUnit,
3116531176) !void {
3116631177 const zcu = sema.pt.zcu;
31178 const ip = &zcu.intern_pool;
31179 switch (referenced_unit.unwrap()) {
31180 .func => |f| assert(ip.unwrapCoercedFunc(f) == f), // for `.{ .func = f }`, `f` must be uncoerced
31181 else => {},
31182 }
3116731183 if (!zcu.comp.incremental and zcu.comp.reference_trace == 0) return;
3116831184 const gop = try sema.references.getOrPut(sema.gpa, referenced_unit);
3116931185 if (gop.found_existing) return;
......@@ -31350,8 +31366,9 @@ fn maybeQueueFuncBodyAnalysis(sema: *Sema, block: *Block, src: LazySrcLoc, nav_i
3135031366 const nav_val = zcu.navValue(nav_index);
3135131367 if (!ip.isFuncBody(nav_val.toIntern())) return;
3135231368
31353 try sema.addReferenceEntry(block, src, AnalUnit.wrap(.{ .func = nav_val.toIntern() }));
31354 try zcu.ensureFuncBodyAnalysisQueued(nav_val.toIntern());
31369 const orig_fn_index = ip.unwrapCoercedFunc(nav_val.toIntern());
31370 try sema.addReferenceEntry(block, src, .wrap(.{ .func = orig_fn_index }));
31371 try zcu.ensureFuncBodyAnalysisQueued(orig_fn_index);
3135531372}
3135631373
3135731374fn analyzeRef(
......@@ -34984,8 +35001,9 @@ fn resolveInferredErrorSet(
3498435001 }
3498535002 // In this case we are dealing with the actual InferredErrorSet object that
3498635003 // corresponds to the function, not one created to track an inline/comptime call.
34987 try sema.addReferenceEntry(block, src, AnalUnit.wrap(.{ .func = func_index }));
34988 try pt.ensureFuncBodyUpToDate(func_index);
35004 const orig_func_index = ip.unwrapCoercedFunc(func_index);
35005 try sema.addReferenceEntry(block, src, .wrap(.{ .func = orig_func_index }));
35006 try pt.ensureFuncBodyUpToDate(orig_func_index);
3498935007 }
3499035008
3499135009 // This will now have been resolved by the logic at the end of `Zcu.analyzeFnBody`
src/Zcu.zig+3
......@@ -3451,8 +3451,11 @@ pub fn mapOldZirToNew(
34513451/// will be analyzed when it returns: for that, see `ensureFuncBodyAnalyzed`.
34523452pub fn ensureFuncBodyAnalysisQueued(zcu: *Zcu, func_index: InternPool.Index) !void {
34533453 const ip = &zcu.intern_pool;
3454
34543455 const func = zcu.funcInfo(func_index);
34553456
3457 assert(func.ty == func.uncoerced_ty); // analyze the body of the original function, not a coerced one
3458
34563459 if (zcu.func_body_analysis_queued.contains(func_index)) return;
34573460
34583461 if (func.analysisUnordered(ip).is_analyzed) {
src/Zcu/PerThread.zig+5-5
......@@ -1571,7 +1571,7 @@ fn analyzeNavType(pt: Zcu.PerThread, nav_id: InternPool.Nav.Index) Zcu.CompileEr
15711571 return .{ .type_changed = true };
15721572}
15731573
1574pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: InternPool.Index) Zcu.SemaError!void {
1574pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, func_index: InternPool.Index) Zcu.SemaError!void {
15751575 dev.check(.sema);
15761576
15771577 const tracy = trace(@src());
......@@ -1581,15 +1581,15 @@ pub fn ensureFuncBodyUpToDate(pt: Zcu.PerThread, maybe_coerced_func_index: Inter
15811581 const gpa = zcu.gpa;
15821582 const ip = &zcu.intern_pool;
15831583
1584 _ = zcu.func_body_analysis_queued.swapRemove(maybe_coerced_func_index);
1584 _ = zcu.func_body_analysis_queued.swapRemove(func_index);
15851585
1586 // We only care about the uncoerced function.
1587 const func_index = ip.unwrapCoercedFunc(maybe_coerced_func_index);
15881586 const anal_unit: AnalUnit = .wrap(.{ .func = func_index });
15891587
15901588 log.debug("ensureFuncBodyUpToDate {f}", .{zcu.fmtAnalUnit(anal_unit)});
15911589
1592 const func = zcu.funcInfo(maybe_coerced_func_index);
1590 const func = zcu.funcInfo(func_index);
1591
1592 assert(func.ty == func.uncoerced_ty); // analyze the body of the original function, not a coerced one
15931593
15941594 const was_outdated = zcu.outdated.swapRemove(anal_unit) or
15951595 zcu.potentially_outdated.swapRemove(anal_unit);
test/cases/export_from_body_of_coerced_fn.zig created+19
......@@ -0,0 +1,19 @@
1fn original() usize {
2 _ = struct {
3 export const val: u32 = 123;
4 };
5 return 0;
6}
7
8pub fn main() void {
9 const coerced: fn () u64 = original;
10 _ = coerced();
11
12 const S = struct {
13 extern const val: u32;
14 };
15 if (S.val != 123) @panic("wrong value");
16}
17
18// run
19// target=x86_64-linux