Liveness: improve logging

This commit is contained in:
Justus Klausecker 2026-01-07 17:07:24 +01:00 committed by Matthew Lugg
parent 42dea36ce9
commit 00d4f3c001
No known key found for this signature in database
GPG key ID: 3F5B7DCCBF4AF02E
2 changed files with 43 additions and 35 deletions

View file

@ -176,7 +176,10 @@ pub fn analyze(zcu: *Zcu, air: Air, intern_pool: *InternPool) Allocator.Error!Li
data.old_extra = a.extra;
a.extra = .{};
try analyzeBody(&a, .main_analysis, &data, main_body);
assert(data.live_set.count() == 0);
if (std.debug.runtime_safety and data.live_set.count() != 0) {
log.debug("instructions still in live set after analysis: {f}", .{fmtInstSet(&data.live_set)});
@panic("liveness analysis failed");
}
}
return .{
@ -825,10 +828,10 @@ fn analyzeOperands(
// This logic must synchronize with `will_die_immediately` in `AnalyzeBigOperands.init`.
const immediate_death = if (data.live_set.remove(inst)) blk: {
log.debug("[{}] %{d}: removed from live set", .{ pass, @intFromEnum(inst) });
log.debug("[{t}] {f}: removed from live set", .{ pass, inst });
break :blk false;
} else blk: {
log.debug("[{}] %{d}: immediate death", .{ pass, @intFromEnum(inst) });
log.debug("[{t}] {f}: immediate death", .{ pass, inst });
break :blk true;
};
@ -849,7 +852,7 @@ fn analyzeOperands(
const mask = @as(Bpi, 1) << @as(OperandInt, @intCast(i));
if ((try data.live_set.fetchPut(gpa, operand, {})) == null) {
log.debug("[{}] %{d}: added %{d} to live set (operand dies here)", .{ pass, @intFromEnum(inst), operand });
log.debug("[{t}] {f}: added {f} to live set (operand dies here)", .{ pass, inst, operand });
tomb_bits |= mask;
}
}
@ -988,19 +991,19 @@ fn analyzeInstBlock(
},
.main_analysis => {
log.debug("[{}] %{f}: block live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: block live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
// We can move the live set because the body should have a noreturn
// instruction which overrides the set.
try data.block_scopes.put(gpa, inst, .{
.live_set = data.live_set.move(),
});
defer {
log.debug("[{}] %{f}: popped block scope", .{ pass, inst });
log.debug("[{t}] {f}: popped block scope", .{ pass, inst });
var scope = data.block_scopes.fetchRemove(inst).?.value;
scope.live_set.deinit(gpa);
}
log.debug("[{}] %{f}: pushed new block scope", .{ pass, inst });
log.debug("[{t}] {f}: pushed new block scope", .{ pass, inst });
try analyzeBody(a, pass, data, body);
// If the block is noreturn, block deaths not only aren't useful, they're impossible to
@ -1027,7 +1030,7 @@ fn analyzeInstBlock(
}
assert(measured_num == num_deaths); // post-live-set should be a subset of pre-live-set
try a.special.put(gpa, inst, extra_index);
log.debug("[{}] %{f}: block deaths are {f}", .{
log.debug("[{t}] {f}: block deaths are {f}", .{
pass,
inst,
fmtInstList(@ptrCast(a.extra.items[extra_index + 1 ..][0..num_deaths])),
@ -1064,7 +1067,7 @@ fn writeLoopInfo(
const block_inst = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(block_inst));
}
log.debug("[{}] %{f}: includes breaks to {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) });
log.debug("[{t}] {f}: includes breaks to {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.breaks) });
// Now we put the live operands from the loop body in too
const num_live = data.live_set.count();
@ -1076,7 +1079,7 @@ fn writeLoopInfo(
const alive = key.*;
a.extra.appendAssumeCapacity(@intFromEnum(alive));
}
log.debug("[{}] %{f}: maintain liveness of {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: maintain liveness of {f}", .{ LivenessPass.loop_analysis, inst, fmtInstSet(&data.live_set) });
try a.special.put(gpa, inst, extra_index);
@ -1117,7 +1120,7 @@ fn resolveLoopLiveSet(
try data.live_set.ensureUnusedCapacity(gpa, @intCast(loop_live.len));
for (loop_live) |alive| data.live_set.putAssumeCapacity(alive, {});
log.debug("[{}] %{f}: block live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: block live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
for (breaks) |block_inst| {
// We might break to this block, so include every operand that the block needs alive
@ -1130,7 +1133,7 @@ fn resolveLoopLiveSet(
}
}
log.debug("[{}] %{f}: loop live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: loop live set is {f}", .{ LivenessPass.main_analysis, inst, fmtInstSet(&data.live_set) });
}
fn analyzeInstLoop(
@ -1168,7 +1171,7 @@ fn analyzeInstLoop(
.live_set = data.live_set.move(),
});
defer {
log.debug("[{}] %{f}: popped loop block scop", .{ pass, inst });
log.debug("[{t}] {f}: popped loop block scop", .{ pass, inst });
var scope = data.block_scopes.fetchRemove(inst).?.value;
scope.live_set.deinit(gpa);
}
@ -1269,13 +1272,13 @@ fn analyzeInstCondBr(
}
}
log.debug("[{}] %{f}: 'then' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(then_mirrored_deaths.items) });
log.debug("[{}] %{f}: 'else' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(else_mirrored_deaths.items) });
log.debug("[{t}] {f}: 'then' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(then_mirrored_deaths.items) });
log.debug("[{t}] {f}: 'else' branch mirrored deaths are {f}", .{ pass, inst, fmtInstList(else_mirrored_deaths.items) });
data.live_set.deinit(gpa);
data.live_set = then_live.move(); // Really the union of both live sets
log.debug("[{}] %{f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
// Write the mirrored deaths to `extra`
const then_death_count = @as(u32, @intCast(then_mirrored_deaths.items.len));
@ -1343,7 +1346,7 @@ fn analyzeInstSwitchBr(
});
}
defer if (is_dispatch_loop) {
log.debug("[{}] %{f}: popped loop block scop", .{ pass, inst });
log.debug("[{t}] {f}: popped loop block scope", .{ pass, inst });
var scope = data.block_scopes.fetchRemove(inst).?.value;
scope.live_set.deinit(gpa);
};
@ -1401,13 +1404,13 @@ fn analyzeInstSwitchBr(
}
for (mirrored_deaths, 0..) |mirrored, i| {
log.debug("[{}] %{f}: case {} mirrored deaths are {f}", .{ pass, inst, i, fmtInstList(mirrored.items) });
log.debug("[{t}] {f}: case {} mirrored deaths are {f}", .{ pass, inst, i, fmtInstList(mirrored.items) });
}
data.live_set.deinit(gpa);
data.live_set = all_alive.move();
log.debug("[{}] %{f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
log.debug("[{t}] {f}: new live set is {f}", .{ pass, inst, fmtInstSet(&data.live_set) });
}
const else_death_count = @as(u32, @intCast(mirrored_deaths[ncases].items.len));
@ -1506,7 +1509,7 @@ fn AnalyzeBigOperands(comptime pass: LivenessPass) type {
.main_analysis => {
if ((try big.data.live_set.fetchPut(gpa, operand, {})) == null) {
log.debug("[{}] %{f}: added %{f} to live set (operand dies here)", .{ pass, big.inst, operand });
log.debug("[{t}] {f}: added {f} to live set (operand dies here)", .{ pass, big.inst, operand });
big.extra_tombs[extra_byte] |= @as(u32, 1) << extra_bit;
}
},
@ -1568,9 +1571,9 @@ const FmtInstSet = struct {
return;
}
var it = val.set.keyIterator();
try w.print("%{f}", .{it.next().?.*});
try w.print("{f}", .{it.next().?.*});
while (it.next()) |key| {
try w.print(" %{f}", .{key.*});
try w.print(" {f}", .{key.*});
}
}
};
@ -1587,9 +1590,9 @@ const FmtInstList = struct {
try w.writeAll("[no instructions]");
return;
}
try w.print("%{f}", .{val.list[0]});
try w.print("{f}", .{val.list[0]});
for (val.list[1..]) |inst| {
try w.print(" %{f}", .{inst});
try w.print(" {f}", .{inst});
}
}
};

View file

@ -73,7 +73,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
.trap, .unreach => {
try self.verifyInstOperands(inst, .{ .none, .none, .none });
// This instruction terminates the function, so everything should be dead
if (self.live.count() > 0) return invalid("%{f}: instructions still alive", .{inst});
if (self.live.count() > 0) return invalid("{f}: instructions still alive", .{inst});
},
// unary
@ -166,7 +166,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
const un_op = data[@intFromEnum(inst)].un_op;
try self.verifyInstOperands(inst, .{ un_op, .none, .none });
// This instruction terminates the function, so everything should be dead
if (self.live.count() > 0) return invalid("%{f}: instructions still alive", .{inst});
if (self.live.count() > 0) return invalid("{f}: instructions still alive", .{inst});
},
.dbg_var_ptr,
.dbg_var_val,
@ -441,7 +441,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
.repeat => {
const repeat = data[@intFromEnum(inst)].repeat;
const expected_live = self.loops.get(repeat.loop_inst) orelse
return invalid("%{d}: loop %{d} not in scope", .{ @intFromEnum(inst), @intFromEnum(repeat.loop_inst) });
return invalid("{f}: loop {f} not in scope", .{ inst, repeat.loop_inst });
try self.verifyMatchingLiveness(repeat.loop_inst, expected_live);
},
@ -451,7 +451,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
try self.verifyOperand(inst, br.operand, self.liveness.operandDies(inst, 0));
const expected_live = self.loops.get(br.block_inst) orelse
return invalid("%{d}: loop %{d} not in scope", .{ @intFromEnum(inst), @intFromEnum(br.block_inst) });
return invalid("{f}: loop {f} not in scope", .{ inst, br.block_inst });
try self.verifyMatchingLiveness(br.block_inst, expected_live);
},
@ -487,7 +487,12 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
if (ip.isNoReturn(block_ty.toIntern())) {
assert(!self.blocks.contains(inst));
} else {
var live = self.blocks.fetchRemove(inst).?.value;
var live = if (self.blocks.fetchRemove(inst)) |kv| kv.value else {
return invalid(
"{f}: block of type '{f}' not terminated correctly",
.{ inst, block_ty.fmtDebug() },
);
};
defer live.deinit(self.gpa);
try self.verifyMatchingLiveness(inst, live);
@ -502,7 +507,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
// The same stuff should be alive after the loop as before it.
const gop = try self.loops.getOrPut(self.gpa, inst);
if (gop.found_existing) return invalid("%{d}: loop already exists", .{@intFromEnum(inst)});
if (gop.found_existing) return invalid("{f}: loop already exists", .{inst});
defer {
var live = self.loops.fetchRemove(inst).?;
live.value.deinit(self.gpa);
@ -551,7 +556,7 @@ fn verifyBody(self: *Verify, body: []const Air.Inst.Index) Error!void {
// after the loop as before it.
{
const gop = try self.loops.getOrPut(self.gpa, inst);
if (gop.found_existing) return invalid("%{d}: loop already exists", .{@intFromEnum(inst)});
if (gop.found_existing) return invalid("{f}: loop already exists", .{inst});
gop.value_ptr.* = self.live.move();
}
defer {
@ -606,11 +611,11 @@ fn verifyOperand(self: *Verify, inst: Air.Inst.Index, op_ref: Air.Inst.Ref, dies
return;
};
if (dies) {
if (!self.live.remove(operand)) return invalid("%{f}: dead operand %{f} reused and killed again", .{
if (!self.live.remove(operand)) return invalid("{f}: dead operand {f} reused and killed again", .{
inst, operand,
});
} else {
if (!self.live.contains(operand)) return invalid("%{f}: dead operand %{f} reused", .{ inst, operand });
if (!self.live.contains(operand)) return invalid("{f}: dead operand {f} reused", .{ inst, operand });
}
}
@ -635,9 +640,9 @@ fn verifyInst(self: *Verify, inst: Air.Inst.Index) Error!void {
}
fn verifyMatchingLiveness(self: *Verify, block: Air.Inst.Index, live: LiveMap) Error!void {
if (self.live.count() != live.count()) return invalid("%{f}: different deaths across branches", .{block});
if (self.live.count() != live.count()) return invalid("{f}: different deaths across branches", .{block});
var live_it = self.live.keyIterator();
while (live_it.next()) |live_inst| if (!live.contains(live_inst.*)) return invalid("%{f}: different deaths across branches", .{block});
while (live_it.next()) |live_inst| if (!live.contains(live_inst.*)) return invalid("{f}: different deaths across branches", .{block});
}
fn invalid(comptime fmt: []const u8, args: anytype) error{LivenessInvalid} {