ajhahn.de
← Flash
Zig 193 lines
// flashc — the Flash compiler driver.
//
// Flash is a small systems language whose backend lowers to Zig (the
// "Tier 0" strategy): the long game is to rewrite FlashOS and
// its shell in Flash, reusing the Zig toolchain for code generation while
// Flash and Zig sources coexist module by module during the migration.
//
// This driver wires the pipeline — lex -> parse -> sema -> lower — end to
// end: it reads a .flash file, parses it, runs the semantic checks,
// and writes the lowered Zig to stdout. Tokens can still be inspected on
// their own with `--dump-tokens`, and a file can be reformatted in place with
// `fmt`. Parse and semantic diagnostics are written to stderr, so the lowered
// source on stdout stays clean for redirection (`flashc file.flash > out.zig`).
//
// The `main(init: std.process.Init)` entry, the `std.Io` reader/writer
// interfaces, and `init.minimal.args` follow the host-tool conventions in
// the FlashOS tree (scripts/generate_syms.zig, tools/gen_shadow.zig).
//
// Usage:
//   flashc --version
//   flashc --dump-tokens <file.flash>
//   flashc fmt [--check] <file.flash>   (reformat a .flash file in place)
//   flashc <file.flash>                 (transpile to Zig, written to stdout)

const std = @import("std");
const Io = std.Io;
const build_options = @import("build_options");
const Lexer = @import("lexer.zig").Lexer;
const parser = @import("parser.zig");
const sema = @import("sema.zig");
const lower = @import("lower.zig");
const fmt = @import("fmt.zig");

const usage =
    \\flashc — the Flash compiler (Flash -> Zig)
    \\
    \\usage:
    \\  flashc --version
    \\  flashc --dump-tokens <file.flash>
    \\  flashc fmt [--check] <file.flash>
    \\  flashc <file.flash>
    \\
;

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 {};

    var stderr_buf: [4096]u8 = undefined;
    var stderr_obj = std.Io.File.stderr().writer(io, &stderr_buf);
    const err_out = &stderr_obj.interface;
    defer err_out.flush() catch {};

    const args = try init.minimal.args.toSlice(arena);
    if (args.len < 2) {
        try out.writeAll(usage);
        return error.NoArguments;
    }
    const cmd = args[1];

    if (std.mem.eql(u8, cmd, "--version")) {
        try out.print("flashc {s}\n", .{build_options.version});
        return;
    }

    if (std.mem.eql(u8, cmd, "--help") or std.mem.eql(u8, cmd, "-h")) {
        try out.writeAll(usage);
        return;
    }

    if (std.mem.eql(u8, cmd, "--dump-tokens")) {
        if (args.len < 3) {
            try out.writeAll("--dump-tokens needs a file\n");
            return error.NoInput;
        }
        try dumpTokens(out, try readFile(io, arena, args[2]));
        return;
    }

    if (std.mem.eql(u8, cmd, "fmt")) {
        // `flashc fmt <file>` rewrites the file to canonical Flash; `--check`
        // writes nothing and exits non-zero when the file would change.
        var check_mode = false;
        var fmt_path: ?[]const u8 = null;
        var ai: usize = 2;
        while (ai < args.len) : (ai += 1) {
            const a = args[ai];
            if (std.mem.eql(u8, a, "--check")) {
                check_mode = true;
            } else if (fmt_path == null) {
                fmt_path = a;
            }
        }
        const fpath = fmt_path orelse {
            try err_out.writeAll("fmt needs a file\n");
            try err_out.flush();
            return error.NoInput;
        };
        const fsrc = try readFile(io, arena, fpath);
        var fp = parser.Parser.init(arena, fsrc);
        const fprog = fp.parseProgram() catch |err| switch (err) {
            // A parse error refuses the file untouched — a formatter must never
            // destroy code. Print the one-line diagnostic and exit non-zero.
            error.UnexpectedToken => {
                if (fp.diag) |d| {
                    try err_out.print("flashc: {s}:{d}: error: {s}\n", .{ fpath, d.line, d.msg });
                } else {
                    try err_out.print("flashc: {s}: parse error\n", .{fpath});
                }
                try err_out.flush();
                std.process.exit(1);
            },
            else => return err,
        };
        const formatted = try fmt.render(arena, fprog, fp.comments, fsrc);
        if (std.mem.eql(u8, formatted, fsrc)) return; // already canonical
        if (check_mode) {
            // Not canonical: report the path and exit non-zero, writing nothing.
            // exit() skips deferred flushes, so flush stdout explicitly.
            try out.print("{s}\n", .{fpath});
            try out.flush();
            std.process.exit(1);
        }
        try std.Io.Dir.cwd().writeFile(io, .{ .sub_path = fpath, .data = formatted });
        return;
    }

    // Otherwise treat the argument as an input file and run the pipeline.
    const path = cmd;
    const src = try readFile(io, arena, path);

    var p = parser.Parser.init(arena, src);
    const program = p.parseProgram() catch |err| switch (err) {
        // A user-facing syntax error: print the one-line diagnostic and exit
        // non-zero. exit() skips deferred flushes, so flush stderr explicitly.
        error.UnexpectedToken => {
            if (p.diag) |d| {
                try err_out.print("flashc: {s}:{d}: error: {s}\n", .{ path, d.line, d.msg });
            } else {
                try err_out.print("flashc: {s}: parse error\n", .{path});
            }
            try err_out.flush();
            std.process.exit(1);
        },
        else => return err, // OutOfMemory and the like are exceptional
    };

    const diags = try sema.check(arena, program);
    if (diags.len > 0) {
        // Report every diagnostic in source order (by anchor offset), then exit
        // non-zero. exit() skips deferred flushes, so flush stderr explicitly.
        std.mem.sort(sema.Diag, diags, src, lessByAnchor);
        for (diags) |d| {
            const loc = sema.locate(src, d.anchor);
            try err_out.print("flashc: {s}:{d}:{d}: error: {s}\n", .{ path, loc.line, loc.col, d.msg });
            if (d.note_anchor) |na| {
                const nloc = sema.locate(src, na);
                try err_out.print("flashc: {s}:{d}:{d}: note: {s}\n", .{ path, nloc.line, nloc.col, d.note_msg.? });
            }
        }
        try err_out.flush();
        std.process.exit(1);
    }

    const zig_src = try lower.emit(arena, program);
    try out.writeAll(zig_src);
}

// Order diagnostics by the byte offset of their anchor in the source, so they
// print top-to-bottom regardless of the order the checker collected them.
fn lessByAnchor(src: []const u8, a: sema.Diag, b: sema.Diag) bool {
    _ = src;
    return @intFromPtr(a.anchor.ptr) < @intFromPtr(b.anchor.ptr);
}

fn readFile(io: Io, arena: std.mem.Allocator, path: []const u8) ![]u8 {
    return std.Io.Dir.cwd().readFileAlloc(io, path, arena, .limited(1 << 20));
}

fn dumpTokens(out: *Io.Writer, src: []const u8) !void {
    var lx = Lexer.init(src);
    while (true) {
        const t = lx.next();
        try out.print("{d:>4}  {s:<12} {s}\n", .{ t.line, @tagName(t.kind), t.lexeme(src) });
        if (t.kind == .eof) break;
    }
}