Skip to content

Commit

Permalink
Merge pull request #7573 from roc-lang/zig-cli
Browse files Browse the repository at this point in the history
Improve roc cli
  • Loading branch information
lukewilliamboswell authored Feb 4, 2025
2 parents 015e30b + 2a7713e commit fb6bf56
Show file tree
Hide file tree
Showing 4 changed files with 299 additions and 84 deletions.
28 changes: 19 additions & 9 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,13 @@ pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});

const main_path = b.path("src/main.zig");

const exe = b.addExecutable(.{
.name = "roc",
.root_source_file = main_path,
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});

const main_tests = b.addTest(.{ .root_source_file = main_path });
const test_cmd = b.addRunArtifact(main_tests);

const test_step = b.step("test", "Run tests");
test_step.dependOn(&test_cmd.step);

b.installArtifact(exe);

const run_cmd = b.addRunArtifact(exe);
Expand All @@ -31,4 +23,22 @@ pub fn build(b: *std.Build) void {

const run_step = b.step("run", "Build and run the roc cli");
run_step.dependOn(&run_cmd.step);

const all_tests = b.addTest(.{
.root_source_file = b.path("src/test.zig"),
.target = target,
.optimize = optimize,
});

// Install the test binary so we can run separately
// ```sh
// $ zig build && ./zig-out/bin/test
// ```
b.installArtifact(all_tests);

const run_tests = b.addRunArtifact(all_tests);

const test_step = b.step("test", "Run all tests included in src/tests.zig");

test_step.dependOn(&run_tests.step);
}
198 changes: 198 additions & 0 deletions src/command.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
const std = @import("std");
const testing = std.testing;
const mem = std.mem;

pub const RocCmd = enum {
roc_run,
roc_build,
roc_test,
roc_repl,
roc_format,
roc_version,
roc_check,
roc_docs,
roc_glue,
roc_help,

pub fn parse(str: []const u8) ?RocCmd {
const map = std.static_string_map.StaticStringMap(RocCmd).initComptime(.{
.{ "run", .roc_run },
.{ "build", .roc_build },
.{ "test", .roc_test },
.{ "repl", .roc_repl },
.{ "format", .roc_format },
.{ "version", .roc_version },
.{ "check", .roc_check },
.{ "docs", .roc_docs },
.{ "glue", .roc_glue },
.{ "help", .roc_help },
});

if (map.get(str)) |cmd| {
return cmd;
}
return null;
}
};

test "parse cli subcommands" {
try testing.expectEqual(RocCmd.parse("build").?, .roc_build);
try testing.expectEqual(RocCmd.parse(""), null);
}

pub const RocOpt = struct {
opt: enum {
none,
size,
speed,
} = .none,
emit_llvm_ir: bool = false,
profiling: bool = false,
timing: bool = false,
fuzzing: bool = false,

pub fn parse(args: []const []const u8) !struct { opt: RocOpt, next_index: usize } {
var opt = RocOpt{};
var i: usize = 0;
while (i < args.len) : (i += 1) {
const arg = args[i];

// If argument doesn't start with '-', we're done parsing options
if (arg.len == 0 or arg[0] != '-') {
return .{ .opt = opt, .next_index = i };
}

if (mem.eql(u8, arg, "--opt")) {
// Check if there's a next argument
if (i + 1 >= args.len) {
return error.MissingOptValue;
}
i += 1;
const value = args[i];

if (mem.eql(u8, value, "none")) {
opt.opt = .none;
} else if (mem.eql(u8, value, "size")) {
opt.opt = .size;
} else if (mem.eql(u8, value, "speed")) {
opt.opt = .speed;
} else {
return error.InvalidOptValue;
}
} else if (mem.eql(u8, arg, "--emit-llvm-ir")) {
opt.emit_llvm_ir = true;
} else if (mem.eql(u8, arg, "--profiling")) {
opt.profiling = true;
} else if (mem.eql(u8, arg, "--time")) {
opt.timing = true;
} else if (mem.eql(u8, arg, "--fuzz")) {
opt.fuzzing = true;
} else {
return error.InvalidArgument;
}
}

return .{ .opt = opt, .next_index = i };
}
};

test "default options" {
try testing.expectEqual(RocOpt{}, RocOpt{
.opt = .none,
.emit_llvm_ir = false,
.profiling = false,
.timing = false,
.fuzzing = false,
});
}

// Testing helper, split a string into arguments ignoring whitespace and empty strings
fn splitArgs(allocator: std.mem.Allocator, str: []const u8) ![]const []const u8 {
var args = std.ArrayList([]const u8).init(allocator);
errdefer args.deinit();

var iter = std.mem.split(u8, str, " ");
while (iter.next()) |arg| {
if (arg.len > 0) {
try args.append(arg);
}
}

return args.toOwnedSlice();
}

test "parsing cli options" {
const TestCase = struct {
args: []const u8,
expected: union(enum) {
ok: struct {
opt: RocOpt,
next_index: usize,
},
err: anyerror,
},
};

const test_cases = &[_]TestCase{
.{
.args = "",
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
},
.{
.args = "--opt size",
.expected = .{ .ok = .{ .opt = RocOpt{ .opt = .size }, .next_index = 2 } },
},
.{
.args = "--opt speed app.roc",
.expected = .{ .ok = .{ .opt = RocOpt{ .opt = .speed }, .next_index = 2 } },
},
.{
.args = "--time build app.roc",
.expected = .{ .ok = .{ .opt = RocOpt{ .timing = true }, .next_index = 1 } },
},
.{
.args = "--opt size --time app.roc",
.expected = .{ .ok = .{
.opt = RocOpt{ .opt = .size, .timing = true },
.next_index = 3,
} },
},
.{
.args = "build --time",
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
},
.{
.args = "--opt",
.expected = .{ .err = error.MissingOptValue },
},
.{
.args = "--opt invalid",
.expected = .{ .err = error.InvalidOptValue },
},
.{
.args = "--unknown-flag",
.expected = .{ .err = error.InvalidArgument },
},
.{
.args = "app.roc --invalid",
.expected = .{ .ok = .{ .opt = RocOpt{}, .next_index = 0 } },
},
};

for (test_cases) |tc| {
const args = try splitArgs(testing.allocator, tc.args);
defer testing.allocator.free(args);

switch (tc.expected) {
.ok => |expected| {
const result = try RocOpt.parse(args);
try testing.expectEqual(expected.opt, result.opt);
try testing.expectEqual(expected.next_index, result.next_index);
},
.err => |expected_err| {
const result = RocOpt.parse(args);
try testing.expectError(expected_err, result);
},
}
}
}
Loading

0 comments on commit fb6bf56

Please sign in to comment.