ajhahn.de
← Flash
Zig 121 lines
// 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 <stage1-flashc> <stage2-flashc> <dir> [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 <stage1-flashc> <stage2-flashc> <dir> [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;
}