// fixpoint — the stage1-vs-stage2 bootstrap fixpoint harness. // // Runs the stage1 and stage2 flashc binaries in transpile mode over every // .flash file in the given directories, twice each, and asserts: // // * determinism — each binary emits identical bytes across its two runs // * the fixpoint — stage1 and stage2 emit byte-identical Zig // // Every file must transpile cleanly (exit 0) in all four runs: the corpus // here is the accepted surface (the selfhost sources and the examples), so // a rejection is itself a failure. The comparison is over the emitted bytes // on stdout — the bootstrap contract: the compiler the Flash sources // describe rebuilds itself exactly, byte for byte. // // Usage: fixpoint [dir ...] const std = @import("std"); const Io = std.Io; pub fn main(init: std.process.Init) !void { const io = init.io; const arena = init.arena.allocator(); var stdout_buf: [4096]u8 = undefined; var stdout_obj = std.Io.File.stdout().writer(io, &stdout_buf); const out = &stdout_obj.interface; defer out.flush() catch {}; const args = try init.minimal.args.toSlice(arena); if (args.len < 4) { try out.writeAll("usage: fixpoint [dir ...]\n"); return error.BadArguments; } const stage1 = args[1]; const stage2 = args[2]; var failures: usize = 0; var files: usize = 0; for (args[3..]) |dir_path| { // Collect the directory's .flash entries and sort them, so the // report order is stable across runs and platforms. var dir = try Io.Dir.cwd().openDir(io, dir_path, .{ .iterate = true }); defer dir.close(io); var names: std.ArrayList([]const u8) = .empty; var it = dir.iterate(); while (try it.next(io)) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.name, ".flash")) continue; try names.append(arena, try arena.dupe(u8, entry.name)); } std.mem.sort([]const u8, names.items, {}, lessThanStr); for (names.items) |name| { const path = try std.fs.path.join(arena, &.{ dir_path, name }); files += 1; failures += try checkFile(arena, io, out, stage1, stage2, path); } } if (failures > 0) { try out.print("fixpoint: {d} failure(s) across {d} files\n", .{ failures, files }); try out.flush(); std.process.exit(1); } try out.print("fixpoint: {d} files, stage1 == stage2, deterministic\n", .{files}); } fn lessThanStr(_: void, a: []const u8, b: []const u8) bool { return std.mem.lessThan(u8, a, b); } // One clean transpile: the emitted Zig on stdout, or null (reported) if the // run did not exit 0. fn emit( arena: std.mem.Allocator, io: Io, out: *Io.Writer, binary: []const u8, tag: []const u8, path: []const u8, ) !?[]u8 { const r = try std.process.run(arena, io, .{ .argv = &.{ binary, path } }); const clean = switch (r.term) { .exited => |code| code == 0, else => false, }; if (!clean) { try out.print("fixpoint: {s}: {s} did not transpile cleanly\n", .{ path, tag }); return null; } return r.stdout; } // Check one source file: two runs per binary, three byte-equality // assertions. Returns 1 on any failure, 0 when all hold. fn checkFile( arena: std.mem.Allocator, io: Io, out: *Io.Writer, stage1: []const u8, stage2: []const u8, path: []const u8, ) !usize { const a1 = (try emit(arena, io, out, stage1, "stage1", path)) orelse return 1; const a2 = (try emit(arena, io, out, stage1, "stage1", path)) orelse return 1; const b1 = (try emit(arena, io, out, stage2, "stage2", path)) orelse return 1; const b2 = (try emit(arena, io, out, stage2, "stage2", path)) orelse return 1; if (!std.mem.eql(u8, a1, a2)) { try out.print("fixpoint: {s}: stage1 is nondeterministic\n", .{path}); return 1; } if (!std.mem.eql(u8, b1, b2)) { try out.print("fixpoint: {s}: stage2 is nondeterministic\n", .{path}); return 1; } if (!std.mem.eql(u8, a1, b1)) { try out.print("fixpoint: {s}: stage1 and stage2 emit different bytes\n", .{path}); return 1; } return 0; }