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