Replace structural Air/IP comparison with text-based dumpers

Remove all @import("zig_internals") from stage0/ so that test_obj
compilation is independent of the Zig compiler (~6min). The sema
comparison now uses text-based dumpers:

- Zig side (src/verbose_air.zig): compiles source through the full Zig
  pipeline, captures verbose_air output, exports zig_dump_air() as a C
  function. Compiled as a separate dumper_obj that is cached
  independently.

- C side (stage0/verbose_air.c): formats C Air structs to text in the
  same format as Zig's Air/print.zig.

Changing stage0 code no longer triggers Zig compiler recompilation:
C compile + cached test_obj + cached dumper + link = seconds.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-19 14:48:40 +00:00
parent 0d15ca3dd4
commit 00a6cb4fd4
11 changed files with 668 additions and 561 deletions

174
src/verbose_air.zig Normal file
View File

@@ -0,0 +1,174 @@
// verbose_air.zig — Zig-side dumper for Air text output.
// Compiles source via the Zig compiler pipeline and captures verbose_air output.
// Exports C-compatible functions for use by stage0 tests.
const std = @import("std");
const zig_internals = @import("zig_internals");
const Compilation = zig_internals.Compilation;
const Package = zig_internals.Package;
comptime {
_ = @import("verbose_intern_pool.zig");
}
const DumpResult = extern struct {
text: ?[*:0]u8,
error_msg: ?[*:0]u8,
};
/// Compile the source file at `src_path` (relative to cwd) through the Zig
/// pipeline and return the verbose_air text output.
/// func_filter: NULL=all functions, "foo"=only functions containing 'foo'.
export fn zig_dump_air(
src_path_ptr: [*:0]const u8,
func_filter: ?[*:0]const u8,
) DumpResult {
return zigDumpAirImpl(std.mem.span(src_path_ptr), func_filter) catch |err| {
return errResult(@errorName(err));
};
}
fn errResult(msg: []const u8) DumpResult {
const duped = std.heap.c_allocator.dupeZ(u8, msg) catch
return .{ .text = null, .error_msg = null };
return .{ .text = null, .error_msg = duped.ptr };
}
fn zigDumpAirImpl(src_path: []const u8, func_filter: ?[*:0]const u8) !DumpResult {
const gpa = std.heap.c_allocator;
var arena_state = std.heap.ArenaAllocator.init(gpa);
defer arena_state.deinit();
const arena = arena_state.allocator();
var dirs: Compilation.Directories = .init(
arena,
"lib/",
null,
.search,
{},
"",
);
defer dirs.deinit();
// Hardcode x86_64-linux-musl target.
const resolved_target: Package.Module.ResolvedTarget = .{
.result = try std.zig.system.resolveTargetQuery(.{
.cpu_arch = .x86_64,
.os_tag = .linux,
.abi = .musl,
}),
.is_native_os = false,
.is_native_abi = false,
.is_explicit_dynamic_linker = false,
};
const config = try Compilation.Config.resolve(.{
.output_mode = .Obj,
.resolved_target = resolved_target,
.have_zcu = true,
.emit_bin = false,
.is_test = false,
});
// Split src_path into directory and filename for the Module.
// Use .root = .none with absolute path to avoid the source being
// associated with zig_lib (which would conflict with the std module
// when compiling files under lib/).
const src_dir = std.fs.path.dirname(src_path) orelse ".";
const src_basename = std.fs.path.basename(src_path);
const abs_src_dir = try std.fs.cwd().realpathAlloc(arena, src_dir);
const root_path: Compilation.Path = .{ .root = .none, .sub_path = abs_src_dir };
const root_mod = try Package.Module.create(arena, .{
.paths = .{
.root = root_path,
.root_src_path = src_basename,
},
.fully_qualified_name = "root",
.cc_argv = &.{},
.inherited = .{
.resolved_target = resolved_target,
},
.global = config,
.parent = null,
});
// Heap-allocate the thread pool so its address stays stable.
const thread_pool = try gpa.create(std.Thread.Pool);
thread_pool.* = undefined;
try thread_pool.init(.{
.allocator = gpa,
.n_jobs = 1,
.track_ids = true,
.stack_size = 60 << 20,
});
defer {
thread_pool.deinit();
gpa.destroy(thread_pool);
}
var create_diag: Compilation.CreateDiagnostic = undefined;
const comp = Compilation.create(gpa, arena, &create_diag, .{
.dirs = dirs,
.root_name = "test",
.config = config,
.root_mod = root_mod,
.emit_bin = .no,
.thread_pool = thread_pool,
.cache_mode = .whole,
.verbose_air = true,
}) catch |err| switch (err) {
error.CreateFail => {
return errResult("Compilation.create failed");
},
else => return err,
};
defer comp.destroy();
// Capture per-function Air text via the verbose_air_output mechanism.
var air_output: std.io.Writer.Allocating = .init(gpa);
defer air_output.deinit();
comp.verbose_air_output = &air_output.writer;
try comp.update(std.Progress.Node.none);
var error_bundle = try comp.getAllErrorsAlloc();
defer error_bundle.deinit(gpa);
if (error_bundle.errorMessageCount() > 0) {
return errResult("compilation produced errors");
}
const full_text = air_output.written();
// Filter by function name if specified.
if (func_filter) |filter_ptr| {
const filter_str = std.mem.span(filter_ptr);
var filtered: std.ArrayListUnmanaged(u8) = .empty;
defer filtered.deinit(gpa);
const begin_marker = "# Begin Function AIR: ";
const end_marker = "# End Function AIR: ";
var pos: usize = 0;
while (std.mem.indexOfPos(u8, full_text, pos, begin_marker)) |begin| {
const fqn_start = begin + begin_marker.len;
const fqn_end = std.mem.indexOfPos(u8, full_text, fqn_start, ":\n") orelse break;
const fqn = full_text[fqn_start..fqn_end];
const end_search = std.mem.indexOfPos(u8, full_text, fqn_end, end_marker) orelse break;
const line_end = std.mem.indexOfPos(u8, full_text, end_search, "\n\n") orelse full_text.len;
const section_end = @min(line_end + 2, full_text.len);
if (std.mem.indexOf(u8, fqn, filter_str) != null) {
try filtered.appendSlice(gpa, full_text[begin..section_end]);
}
pos = section_end;
}
const result = try gpa.dupeZ(u8, filtered.items);
return .{ .text = result.ptr, .error_msg = null };
} else {
const result = try gpa.dupeZ(u8, full_text);
return .{ .text = result.ptr, .error_msg = null };
}
}

View File

@@ -0,0 +1,23 @@
// verbose_intern_pool.zig — Zig-side dumper for InternPool text output.
// Compiles source via the Zig compiler pipeline and dumps the InternPool.
// Exports a C-compatible function for use by stage0 tests.
const std = @import("std");
const DumpResult = extern struct {
text: ?[*:0]u8,
error_msg: ?[*:0]u8,
};
export fn zig_dump_intern_pool(
source_ptr: [*]const u8,
source_len: usize,
) DumpResult {
// Stub: not yet implemented.
_ = source_ptr;
_ = source_len;
const gpa = std.heap.c_allocator;
const result = gpa.dupeZ(u8, "") catch
return .{ .text = null, .error_msg = null };
return .{ .text = result.ptr, .error_msg = null };
}