stages_test: full per-function Air comparison between C and Zig sema

Replace the count-only check with a faithful textual comparison,
analogous to how expectEqualZir compares AstGen output:

- Export Zcu from test_exports so tests can construct a PerThread
- Parse Zig verbose_air output into per-function sections keyed by FQN
- For each C function Air, render it as text via air.write() using
  the Zig PerThread (InternPool indices must match between C and Zig
  for the same source), then compare against the Zig reference text

For the current corpus (codecs.zig, no functions), both sides produce
zero entries so the comparison loop is empty. When zirFunc is ported
and a corpus file with functions is added, this will exercise real
per-function Air matching.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 13:04:30 +00:00
parent 5d05f3633f
commit 47c9dd8636
2 changed files with 67 additions and 12 deletions

View File

@@ -2,4 +2,5 @@ pub const InternPool = @import("InternPool.zig");
pub const Air = @import("Air.zig");
pub const Compilation = @import("Compilation.zig");
pub const Package = @import("Package.zig");
pub const Zcu = @import("Zcu.zig");
// Later: pub const Sema = @import("Sema.zig");

View File

@@ -74,24 +74,78 @@ fn stagesCheck(gpa: Allocator, source: [:0]const u8, src_path: []const u8, check
var zig_result = try sema.zigSema(gpa, src_path);
defer zig_result.deinit();
// Compare per-function Air: C and Zig must produce the same count.
// Parse Zig per-function Air sections from captured verbose_air output.
const zig_air_text = zig_result.air_output.written();
const zig_func_count = countAirSections(zig_air_text);
try std.testing.expectEqual(zig_func_count, result.func_airs.len);
var zig_sections = try parseAirSections(gpa, zig_air_text);
defer deinitAirSections(gpa, &zig_sections);
// C and Zig must produce the same number of per-function Airs.
try std.testing.expectEqual(zig_sections.count(), result.func_airs.len);
// Compare per-function Air text.
const Zcu = zig_internals.Zcu;
const pt: Zcu.PerThread = .activate(zig_result.comp.zcu.?, .main);
defer pt.deactivate();
for (result.func_airs) |func_air| {
const zig_section = zig_sections.get(func_air.name) orelse {
std.debug.print("C sema produced function '{s}' not found in Zig sema\n", .{func_air.name});
return error.TestUnexpectedResult;
};
// Render C Air as text using Zig's PerThread. InternPool indices
// must match between C and Zig for the same source — if they don't,
// the text will differ and the test catches the bug.
var c_air_buf: std.io.Writer.Allocating = .init(gpa);
defer c_air_buf.deinit();
func_air.owned_air.air().write(&c_air_buf.writer, pt, null);
try std.testing.expectEqualStrings(zig_section, c_air_buf.written());
}
}
}
/// Count the number of per-function Air sections in verbose_air output.
/// Sections are delimited by "# Begin Function AIR: " markers.
fn countAirSections(text: []const u8) usize {
const marker = "# Begin Function AIR: ";
var count: usize = 0;
/// Parse verbose_air output into per-function sections keyed by FQN.
/// Sections are delimited by "# Begin Function AIR: {fqn}:\n" and
/// "# End Function AIR: {fqn}\n\n" markers.
/// Returns owned keys and values that the caller must free.
fn parseAirSections(gpa: Allocator, text: []const u8) !std.StringHashMapUnmanaged([]const u8) {
const begin_marker = "# Begin Function AIR: ";
const end_marker = "# End Function AIR: ";
var map: std.StringHashMapUnmanaged([]const u8) = .empty;
errdefer deinitAirSections(gpa, &map);
var pos: usize = 0;
while (std.mem.indexOfPos(u8, text, pos, marker)) |begin| {
count += 1;
pos = begin + marker.len;
while (std.mem.indexOfPos(u8, text, pos, begin_marker)) |begin| {
const fqn_start = begin + begin_marker.len;
// FQN ends at ":\n"
const fqn_end = std.mem.indexOfPos(u8, text, fqn_start, ":\n") orelse break;
const fqn = text[fqn_start..fqn_end];
// Body starts after ":\n"
const body_start = fqn_end + 2;
// Find the matching end marker
const end_pos = std.mem.indexOfPos(u8, text, body_start, end_marker) orelse break;
const body = text[body_start..end_pos];
const key = try gpa.dupe(u8, fqn);
errdefer gpa.free(key);
const val = try gpa.dupe(u8, body);
try map.put(gpa, key, val);
pos = end_pos + end_marker.len;
}
return count;
return map;
}
fn deinitAirSections(gpa: Allocator, map: *std.StringHashMapUnmanaged([]const u8)) void {
var it = map.iterator();
while (it.next()) |entry| {
gpa.free(entry.value_ptr.*);
gpa.free(entry.key_ptr.*);
}
map.deinit(gpa);
}
const last_successful_corpus = "../lib/std/crypto/codecs.zig";