Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expand roc format to support directories #7703

Merged
merged 1 commit into from
Mar 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/collections/utils.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ pub fn exitOnOom(err: std.mem.Allocator.Error) noreturn {
const oom_message =
\\I ran out of memory! I can't do anything to recover, so I'm exiting.
\\Try reducing memory usage on your machine and then running again.
\\
;

std.debug.print(oom_message, .{});
std.process.exit(1);
fatal(oom_message, .{});
},
}
}

/// Log a fatal error and exit the process with a non-zero code.
pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.io.getStdErr().writer().print(format, args) catch unreachable;
std.process.exit(1);
}
7 changes: 5 additions & 2 deletions src/coordinate/Filesystem.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ pub fn default() Self {
};
}

/// The max valid file size.
/// Anything larger will fail due to us using u32 offsets.
pub const max_file_size = std.math.maxInt(u32);

/// All errors that can occur when reading a file.
pub const ReadError = std.fs.File.OpenError || std.posix.ReadError || Allocator.Error || error{StreamTooLong};

Expand Down Expand Up @@ -137,8 +141,7 @@ fn readFileDefault(relative_path: []const u8, allocator: std.mem.Allocator) Read
const file = try std.fs.cwd().openFile(relative_path, .{});
defer file.close();

const max_allowed_file_length = std.math.maxInt(usize);
const contents = try file.reader().readAllAlloc(allocator, max_allowed_file_length);
const contents = try file.reader().readAllAlloc(allocator, max_file_size);

return contents;
}
Expand Down
105 changes: 101 additions & 4 deletions src/fmt.zig
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Formatting logic for Roc modules.

const std = @import("std");
const parse = @import("check/parse.zig").parse;
const IR = @import("check/parse/IR.zig");
const Node = IR.Node;
const Filesystem = @import("coordinate/Filesystem.zig");
const tokenizer = @import("check/parse/tokenize.zig");
const TokenizedBuffer = tokenizer.TokenizedBuffer;
const TokenIdx = tokenizer.Token.Idx;
const exitOnOom = @import("./collections/utils.zig").exitOnOom;
const fatal = @import("./collections/utils.zig").fatal;
const base = @import("base.zig");

const NodeStore = IR.NodeStore;
Expand Down Expand Up @@ -42,6 +45,75 @@ pub fn resetWith(fmt: *Formatter, ast: IR) void {
fmt.ast = ast;
}

/// Formats all roc files in the specified path.
/// Handles both single files and directories
/// Returns the number of files formatted.
pub fn formatPath(gpa: std.mem.Allocator, base_dir: std.fs.Dir, path: []const u8) !usize {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not important to integrate with yet, but we have a half-baked filesystem wrapper that will help us with supporting WASI and maybe testing. That would probably be good to discuss the merits of in Zulip before we get too far.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To note, this isn't really changed. I just moved this code. But I agree we should fix it up.

// TODO: update this to use the filesystem abstraction
// When doing so, add a mock filesystem and some tests.
var count: usize = 0;
// First try as a directory.
if (base_dir.openDir(path, .{ .iterate = true })) |const_dir| {
var dir = const_dir;
defer dir.close();
// Walk is recursive.
var walker = try dir.walk(gpa);
defer walker.deinit();
while (try walker.next()) |entry| {
if (entry.kind == .file) {
if (try formatFilePath(gpa, entry.dir, entry.basename)) {
count += 1;
}
}
}
} else |_| {
if (try formatFilePath(gpa, base_dir, path)) {
count += 1;
}
}

return count;
}

fn formatFilePath(gpa: std.mem.Allocator, base_dir: std.fs.Dir, path: []const u8) !bool {
// Skip non ".roc" files.
if (!std.mem.eql(u8, std.fs.path.extension(path), ".roc")) {
return false;
}

const input_file = try base_dir.openFile(path, .{ .mode = .read_only });
defer input_file.close();

const contents = try input_file.reader().readAllAlloc(gpa, Filesystem.max_file_size);
defer gpa.free(contents);

var module_env = base.ModuleEnv.init(gpa);
defer module_env.deinit();

var parse_ast = parse(&module_env, contents);
defer parse_ast.deinit();
if (parse_ast.errors.len > 0) {
// TODO: pretty print the parse failures.
const stderr = std.io.getStdErr().writer();
try stderr.print("Failed to parse '{s}' for formatting.\nErrors:\n", .{path});
for (parse_ast.errors) |err| {
try stderr.print("\t{s}\n", .{@tagName(err.tag)});
}
fatal("\n", .{});
}

var formatter = init(parse_ast);
defer formatter.deinit();

const formatted_output = formatter.formatFile();
defer gpa.free(formatted_output);

const output_file = try base_dir.createFile(path, .{});
defer output_file.close();
try output_file.writeAll(formatted_output);
return true;
}

/// Emits a string containing the well-formed source of a Roc parse IR (AST).
/// The resulting string is owned by the caller.
pub fn formatFile(fmt: *Formatter) []const u8 {
Expand Down Expand Up @@ -728,8 +800,6 @@ fn pushTokenText(fmt: *Formatter, ti: TokenIdx) void {
}

fn moduleFmtsSame(source: []const u8) !void {
const parse = @import("check/parse.zig").parse;

const gpa = std.testing.allocator;

var env = base.ModuleEnv.init(gpa);
Expand Down Expand Up @@ -836,8 +906,6 @@ pub fn moduleFmtsStable(gpa: std.mem.Allocator, input: []const u8, debug: bool)
}

fn parseAndFmt(gpa: std.mem.Allocator, input: []const u8, debug: bool) ![]const u8 {
const parse = @import("check/parse.zig").parse;

var module_env = base.ModuleEnv.init(gpa);
defer module_env.deinit();

Expand Down Expand Up @@ -1064,3 +1132,32 @@ test "Dot access super test" {
const expr = "some_fn(arg1)?.static_dispatch_method()?.next_static_dispatch_method()?.record_field?";
try exprFmtsSame(expr, .no_debug);
}

// TODO: replace this test with one that doesn't interact with the real filesystem.
test "format single file" {
const gpa = std.testing.allocator;
const roc_filename = "test.roc";

const roc_file = try std.fs.cwd().createFile(roc_filename, .{ .read = true });
defer roc_file.close();
try roc_file.writeAll(
\\module []
\\
\\foo = "bar"
);
defer std.fs.cwd().deleteFile(roc_filename) catch std.debug.panic("Failed to clean up test.roc", .{});

const count = try formatPath(gpa, std.fs.cwd(), roc_filename);
try std.testing.expectEqual(1, count);

// Reset file position to read formatted roc code
try roc_file.seekTo(0);
const formatted_code = try roc_file.reader().readAllAlloc(gpa, Filesystem.max_file_size);
defer gpa.free(formatted_code);

try std.testing.expectEqualStrings(
\\module []
\\
\\foo = "bar"
, formatted_code);
}
73 changes: 13 additions & 60 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const std = @import("std");
const fmt = @import("fmt.zig");
const parse = @import("check/parse.zig");
const base = @import("base.zig");
const cli = @import("cli.zig");
const collections = @import("collections.zig");
Expand All @@ -13,6 +12,7 @@ const RocOpt = cli.RocOpt;
const Problem = problem_mod.Problem;
const Allocator = std.mem.Allocator;
const exitOnOom = collections.utils.exitOnOom;
const fatal = collections.utils.fatal;

const usage =
\\Usage:
Expand All @@ -36,12 +36,6 @@ const usage =
\\ -h, --help Print usage
;

/// Log a fatal error and exit the process with a non-zero code.
pub fn fatal(comptime format: []const u8, args: anytype) noreturn {
std.io.getStdErr().writer().print(format, args) catch unreachable;
std.process.exit(1);
}

/// The CLI entrypoint for the Roc compiler.
pub fn main() !void {
const gpa = std.heap.c_allocator;
Expand Down Expand Up @@ -125,62 +119,21 @@ fn rocRepl(gpa: Allocator, opt: RocOpt, args: []const []const u8) !void {
fatal("not implemented", .{});
}

/// Reads, parses, formats, and overwrites a Roc file.
/// Reads, parses, formats, and overwrites all Roc files at the given paths.
/// Recurses into directories to search for Roc files.
fn rocFormat(gpa: Allocator, args: []const []const u8) !void {
const roc_file_path = if (args.len > 0) args[0] else "main.roc";

const input_file = try std.fs.cwd().openFile(roc_file_path, .{ .mode = .read_only });
defer input_file.close();

const contents = try input_file.reader().readAllAlloc(gpa, std.math.maxInt(usize));
defer gpa.free(contents);

var module_env = base.ModuleEnv.init(gpa);
defer module_env.deinit();

var parse_ast = parse.parse(&module_env, contents);
defer parse_ast.deinit();
if (parse_ast.errors.len > 0) {
// TODO: pretty print the parse failures.
fatal("Failed to parse '{s}' for formatting.\nErrors:\n{any}\n", .{ roc_file_path, parse_ast.errors });
// var timer = try std.time.Timer.start();
var count: usize = 0;
if (args.len > 0) {
for (args) |arg| {
count += try fmt.formatPath(gpa, std.fs.cwd(), arg);
}
} else {
count = try fmt.formatPath(gpa, std.fs.cwd(), "main.roc");
}

var formatter = fmt.init(parse_ast);
defer formatter.deinit();

const formatted_output = formatter.formatFile();
defer gpa.free(formatted_output);

const output_file = try std.fs.cwd().createFile(roc_file_path, .{});
defer output_file.close();
try output_file.writeAll(formatted_output);
}

test "format single file" {
const gpa = std.testing.allocator;
const roc_filename = "test.roc";

const roc_file = try std.fs.cwd().createFile(roc_filename, .{ .read = true });
defer roc_file.close();
try roc_file.writeAll(
\\module []
\\
\\foo = "bar"
);
defer std.fs.cwd().deleteFile(roc_filename) catch std.debug.panic("Failed to clean up test.roc", .{});

try rocFormat(gpa, &.{roc_filename});

// Reset file position to read formatted roc code
try roc_file.seekTo(0);
const formatted_code = try roc_file.reader().readAllAlloc(gpa, std.math.maxInt(usize));
defer gpa.free(formatted_code);

try std.testing.expectEqualStrings(
\\module []
\\
\\foo = "bar"
, formatted_code);
// const elapsed = timer.read() / std.time.ns_per_ms;
// try std.io.getStdOut().writer().print("Successfully formatted {} files in {} ms.\n", .{ count, elapsed });
}

fn rocVersion(gpa: Allocator, args: []const []const u8) !void {
Expand Down
Loading