diff --git a/build.zig b/build.zig
index 5161975a0ae6f78d1396e4080eade26a0fe50771..85ee1c9947e81bafd448cefb65153cd242fd11f4 100644
--- a/build.zig
+++ b/build.zig
@@ -25,6 +25,7 @@ const ArtifactGroup = struct {
opts: Options,
cli: *Step.Compile,
+ compiler_cli: *Step.Compile,
format_cli: *Step.Compile,
transform_cli: *Step.Compile,
loader_cli: *Step.Compile,
@@ -58,8 +59,16 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
break :config_mod config.createModule();
};
+ // Cli helper
+ const cli_mod = b.createModule(.{
+ .root_source_file = b.path("src/Cli.zig"),
+ .optimize = options.optimize,
+ .target = options.target,
+ .strip = false,
+ });
+
// Core
- const core_mod = b.addModule("core", .{
+ const core_mod = b.createModule(.{
.root_source_file = b.path("src/core/mod.zig"),
.optimize = options.optimize,
.target = options.target,
@@ -70,7 +79,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
core_mod.addImport("zg:PropsData", zg.module("PropsData"));
// Format
- const format_mod = b.addModule("format", .{
+ const format_mod = b.createModule(.{
.root_source_file = b.path("src/format/Format.zig"),
.optimize = options.optimize,
.target = options.target,
@@ -81,7 +90,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
format_mod.addImport("tracer", tracer.module("tracer"));
// Loader
- const loader_mod = b.addModule("loader", .{
+ const loader_mod = b.createModule(.{
.root_source_file = b.path("src/loader/Loader.zig"),
.optimize = options.optimize,
.target = options.target,
@@ -107,11 +116,12 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
compiler_tests.root_module.addImport("config", config_mod);
compiler_tests.root_module.addImport("tracer", tracer.module("tracer"));
compiler_tests.root_module.addImport("quickjs", quickjs.module("quickjs"));
+ compiler_tests.root_module.addImport("Cli", cli_mod);
// Main CLI
const cli = b.addExecutable(.{
.name = "compiler",
- .root_source_file = b.path("src/cli.zig"),
+ .root_source_file = b.path("src/main.zig"),
.target = options.target,
.optimize = options.optimize,
.use_lld = use_lld,
@@ -125,6 +135,23 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
}
+ // Compiler Dev CLI
+ const compiler_cli = b.addExecutable(.{
+ .name = "compiler",
+ .root_source_file = b.path("src/cli_compiler.zig"),
+ .target = options.target,
+ .optimize = options.optimize,
+ .use_lld = use_lld,
+ .use_llvm = use_llvm,
+ .single_threaded = true,
+ });
+ compiler_cli.root_module.addImport("core", core_mod);
+ compiler_cli.root_module.addImport("config", config_mod);
+ compiler_cli.root_module.addImport("tracer", tracer.module("tracer"));
+ if (options.config.have_embedded_quickjs) {
+ cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
+ }
+
const format_cli = b.addExecutable(.{
.name = "format",
.root_source_file = b.path("src/format/format_cli.zig"),
@@ -154,7 +181,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
const transform_cli = b.addExecutable(.{
.name = "transform",
- .root_source_file = b.path("src/core/transform_cli.zig"),
+ .root_source_file = b.path("src/cli_transform.zig"),
.target = options.target,
.optimize = options.optimize,
.use_lld = use_lld,
@@ -170,6 +197,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
.opts = options,
.cli = cli,
+ .compiler_cli = compiler_cli,
.format_cli = format_cli,
.loader_cli = loader_cli,
.transform_cli = transform_cli,
@@ -372,7 +400,7 @@ pub fn build(b: *Build) !void {
b.installArtifact(main.cli);
inline for (.{
- .{ minimal.cli, "compiler" },
+ .{ minimal.compiler_cli, "compiler" },
.{ minimal.format_cli, "format" },
.{ minimal.loader_cli, "loader" },
.{ minimal.transform_cli, "transform" },
diff --git a/src/cli.zig b/src/cli.zig
index ebbe1e8800327ce7b1a243bec9b8b4780c79dd1d..fd981159f46ab157af9a83a7a403f460203ea21c 100644
--- a/src/cli.zig
+++ b/src/cli.zig
@@ -1,260 +1,656 @@
-//! This is the `lang` command line interface. It combines all components
-//! of the software development kit into one. Multiple versions of this
-//! file are built with different subsets of features, allowing you to,
-//! for example, test the `core` module's compilation features without
-//! needing to compile the formatter.
-
-const have_debug_features = builtin.mode == .Debug;
-const config = @import("config");
-
-pub fn main() !u8 {
- try tracer.init();
- defer tracer.deinit();
- try tracer.init_thread(null);
- defer tracer.deinit_thread();
-
- // unicode props is asserted to be initialized before core.Tokenizer is used
- // this will result in a static page allocation, which is never cleaned up.
- try core.unicode_props.init();
-
- const use_gpa = builtin.mode == .Debug;
- var gpa = if (use_gpa)
- std.heap.GeneralPurposeAllocator(.{
- .stack_trace_frames = 100,
- }){}
- else
- std.heap.ArenaAllocator.init(std.heap.page_allocator);
- var no_leak_checks = false;
- defer _ = if (use_gpa and !no_leak_checks) gpa.deinit();
- const alloc = gpa.allocator();
-
- // Current working directory is required for CLI resolution.
- var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
- const cwd = try std.posix.getcwd(&cwd_buf);
-
- // Command line argument parsing.
- var maybe_file_path: ?[]const u8 = null;
- defer if (maybe_file_path) |path| alloc.free(path);
- var exe_name: ?[]const u8 = null;
- defer if (exe_name) |path| alloc.free(path);
- var dump_ir = false;
- var dump_state: ?[]const u8 = null;
- var run = false;
- var has_errors = false;
- {
- var args = try std.process.argsWithAllocator(alloc);
- defer args.deinit();
-
- exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli"));
-
- while (args.next()) |arg| {
- if (std.mem.startsWith(u8, arg, "-")) {
- if (have_debug_features) {
- if (std.mem.eql(u8, arg, "--report-trace")) {
- core.Report.debug_show_report_trace = true;
- continue;
- }
- if (std.mem.eql(u8, arg, "--no-leak-checks")) {
- no_leak_checks = true;
- continue;
- }
- }
+//! Configuration for an command line interface, which can be executed against
+//! an argument slice using `.parse`. None of the parsing strategies require an
+//! allocator, however it expects incoming arguments to be UTF-8 formatted which
+//! on Windows will require an allocator.
+//!
+//! See `fn main` for a basic usage example.
+options: []const Option = &.{},
+positionals: Positionals = .none,
+help: HelpConfig = .{},
- if (std.mem.eql(u8, arg, "--dump-ir")) {
- dump_ir = true;
- continue;
- }
+const Option = struct {
+ /// Should be formatted as the flag name: '--dump-ir'
+ name: []const u8,
+ /// Displayed in generated help menu
+ desc: []const u8,
+ kind: Kind = .boolean,
+ /// A short code allows single dash argument parsing.
+ short: ?u8 = null,
- if (std.mem.startsWith(u8, arg, "--dump-state=")) {
- dump_state = arg["--dump-state=".len..];
- continue;
- }
+ const Kind = union(enum) {
+ /// UTF-8 validated
+ string,
+ boolean,
+ @"enum": type,
- if (config.have_embedded_quickjs and std.mem.eql(u8, arg, "--run")) {
- run = true;
- continue;
- }
+ pub fn Type(k: Kind) type {
+ return switch (k) {
+ .string => []const u8,
+ .boolean => bool,
+ .@"enum" => |t| t,
+ };
+ }
+ };
- std.debug.print("error: unknown flag '{s}'\n", .{arg});
- has_errors = true;
- break;
- }
+ const help: Option = .{
+ .name = "--help",
+ .short = 'h',
+ .desc = "Print this help and exit",
+ };
+};
- if (maybe_file_path == null) {
- maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg });
- } else {
- std.debug.print("error: multiple files specified\n", .{});
- maybe_file_path = null;
- break;
- }
- }
+pub const Positionals = union(enum) {
+ none,
+ /// Store a single positional: ?[]const u8
+ single,
+ /// Store the start and count of positionals. Call
+ /// 'parsed.positionals.iterator(args)' with the
+ /// original argument array to read positionals.
+ unlimited,
+ /// Same strategy as .unlimited, with a limit
+ limited: usize,
+ /// Use a bounded array to store positionals, using
+ /// a stack allocated buffer that can hold enough
+ /// '[]const u8's as allowed by the bound.
+ bounded: usize,
+
+ pub fn Type(p: Positionals) type {
+ return switch (p) {
+ .none => void,
+ .single => ?[]const u8,
+ .limited => |limit| switch (limit) {
+ 0 => void,
+ else => Multi,
+ },
+ .unlimited => Multi,
+ .bounded => |n| std.BoundedArray([]const u8, n),
+ };
+ }
+
+ pub inline fn maxCount(p: Positionals) usize {
+ return switch (p) {
+ .none => 0,
+ .single => 1,
+ inline .limited, .bounded => |limit| limit,
+ .unlimited => std.math.maxInt(usize),
+ };
}
- if (maybe_file_path == null) has_errors = true;
- if (has_errors) {
- std.debug.print("Usage: {s} [flags] \n", .{exe_name.?});
- {
- std.debug.print(
- // | 60 chars wrap point|
- \\
- \\Flags:
- \\ --dump-ir Dump intermediate representations
- \\ --dump-state= Dump compiler state to JSON
- \\
- , .{});
+ pub const Multi = struct {
+ /// The index of the first positional
+ first: usize,
+ /// The number of total positionals. This can
+ /// be used to pre-allocate resources.
+ len: usize,
+
+ first_is_after_double_dash: bool,
+
+ pub const empty = .{
+ .first = 0,
+ .len = 0,
+ .first_is_after_double_dash = false,
+ };
+
+ pub fn iterator(pos: @This(), all_args: []const []const u8) PositionalIterator {
+ return .{
+ .args = all_args,
+ .i = pos.first,
+ .left = pos.len,
+ .after_double_dash = pos.first_is_after_double_dash,
+ };
}
- if (config.have_embedded_quickjs) {
- std.debug.print(
- \\ --run Run program using QuickJS
- \\
- , .{});
+ };
+};
+
+pub const HelpConfig = struct {
+ /// If set true, registers an addition '--help' / '-h' flag
+ /// These will show the help menu and exit the program.
+ ///
+ /// Help menu can always be displayed manually with .showHelp()
+ option: bool = true,
+
+ /// Override the usage string in the first line.
+ usage: ?[]const u8 = null,
+
+ /// Hide a list of flag names.
+ hide_options: []const []const u8 = &.{},
+};
+
+pub const PositionalIterator = struct {
+ args: []const []const u8,
+ i: usize,
+ left: usize,
+ after_double_dash: bool,
+
+ pub fn peek(it: PositionalIterator) ?[]const u8 {
+ return if (it.left > 0) it.args[it.i] else null;
+ }
+
+ pub fn next(it: *PositionalIterator) ?[]const u8 {
+ if (it.left > 0) {
+ defer it.advance();
+ return it.args[it.i];
+ } else {
+ return null;
}
- if (have_debug_features) {
- std.debug.print(
- \\
- \\Debug Mode Flags:
- \\ --report-trace Print stack traces per error
- \\ --no-leak-checks
- \\
- , .{});
+ }
+
+ pub fn advance(it: *PositionalIterator) void {
+ it.left -= 1;
+ if (it.left == 0) return;
+ if (it.after_double_dash) {
+ it.i += 1;
+ } else while (mem.startsWith(u8, it.args[it.i], "-")) {
+ defer it.i += 1;
+ if (it.args[it.i].len == 2 and it.args[it.i][1] == '-') {
+ it.after_double_dash = true;
+ return;
+ }
}
- std.debug.print("\nLanguage SDK v" ++ config.version_string ++ "\n", .{});
- return 1;
}
- const file_path = maybe_file_path.?;
+};
- // Execute compiler
- {
- var js_backend = core.Js{};
+inline fn fieldName(comptime name: []const u8) [:0]const u8 {
+ std.debug.assert(name[0] == '-' and name[1] == '-');
+ for (name[2..]) |char| switch (char) {
+ 'a'...'z', '-' => {},
+ else => |ch| @compileError(std.fmt.comptimePrint("Unexpected char '{c}' in option name '{s}'", .{ ch, name })),
+ };
- var comp = try core.Comp.init(.{
- .gpa = tracer.tracingAllocator(&alloc),
- .options = .{
- .dump_ir = dump_ir,
- },
- .backend = .{ .js = &js_backend },
- });
- defer comp.deinit();
+ const replacement_size = mem.replacementSize(u8, name[2..], "-", "_");
+ const replaced = brk: {
+ var buf: [replacement_size:0]u8 = undefined;
+ _ = mem.replace(u8, name[2..], "-", "_", &buf);
+ break :brk buf;
+ };
+ return &replaced;
+}
+
+/// Every option name is converted into snake case, then
+/// an additional field name 'positionals' is added.
+pub fn Parsed(p: Cli) type {
+ const fields = brk: {
+ var fields: [p.options.len + 1]Type.StructField = undefined;
- defer if (dump_state) |path| {
- dumpState(&comp, path) catch |err| {
- std.debug.print("Failed to dump compilation state: {}", .{err});
+ for (p.options, fields[0..p.options.len]) |def, *field| {
+ const Wrapped = ?def.kind.Type();
+ field.* = .{
+ .name = fieldName(def.name),
+ .alignment = @alignOf(Wrapped),
+ .default_value = &@as(Wrapped, null),
+ .is_comptime = false,
+ .type = Wrapped,
};
+ }
+
+ const Pos = p.positionals.Type();
+ fields[fields.len - 1] = .{
+ .name = "positionals",
+ .alignment = @alignOf(Pos),
+ .type = Pos,
+ .default_value = &@as(Pos, switch (p.positionals) {
+ .none => {},
+ .single => null,
+ .limited, .unlimited => .empty,
+ .bounded => .{},
+ }),
+ .is_comptime = false,
};
- const main_file = try comp.getFile(file_path);
+ break :brk fields;
+ };
- var fail_report: Report = undefined;
- const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) {
- error.TransformFail => {
- std.debug.print("Transform failed:\n", .{});
- try fail_report.dump(.initComp(&comp, main_file), comp.out);
- fail_report.deinit(comp.gpa);
- return 1;
- },
- error.ParseFail => {
- // Already printed.
- return 1;
- },
- else => return err,
+ return @Type(.{ .@"struct" = .{
+ .layout = .auto,
+ .fields = &fields,
+ .decls = &.{},
+ .is_tuple = false,
+ } });
+}
+
+const StdErrWriter = struct {
+ file: std.fs.File,
+ buffered_writer: std.io.BufferedWriter(4096, std.fs.File.Writer),
+ tty: std.io.tty.Config,
+ writer_ptr: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer,
+
+ pub fn init() StdErrWriter {
+ const stderr = std.io.getStdErr();
+ const tty = std.io.tty.detectConfig(stderr);
+ return .{
+ .file = stderr,
+ .tty = tty,
+ .buffered_writer = std.io.bufferedWriter(stderr.writer()),
+ .writer_ptr = undefined,
};
+ }
- if (comp.options.dump_ir) {
- const stderr = std.io.getStdErr().writer();
- try teg.dumpToWriter(stderr, null);
+ pub fn get(w: *StdErrWriter) std.io.AnyWriter {
+ if (@import("builtin").os.tag == .windows and w.tty == .windows_api) {
+ // all data must be flushed before changing colors using
+ // the Windows API. thankfully, modern windows terminals
+ // will not hit this path.
+ return w.buffered_writer.unbuffered_writer.any();
+ } else {
+ w.writer_ptr = w.buffered_writer.writer(); // uaf unless stored
+ return w.writer_ptr.any();
}
+ }
+};
- const entry_point = try comp.enqueueEntryModule(main_file);
- comp.runAllJobs() catch |err| switch (err) {
- error.AnalysisFail => {
- // error is already printed
- return 1;
- },
- else => return err,
- };
+const ParseError = error{ InvalidArguments, DisplayedHelpMenu };
+
+/// This will quit the program with usage information if invalid arguments are
+/// received and the `--help` option is enabled. This is the most common use
+/// case. If manual error handling is desired, `.parseWithErrorWriter` can
+/// be used instead.
+pub fn parse(
+ comptime p: Cli,
+ program_name: []const u8,
+ args: []const []const u8,
+) p.Parsed() {
+ var stderr = StdErrWriter.init();
+ defer stderr.buffered_writer.flush() catch {};
+ return parseWithErrorWriter(
+ p,
+ program_name,
+ args,
+ stderr.get(),
+ stderr.tty,
+ true,
+ true,
+ ) catch |e| switch (e) {
+ error.InvalidArguments,
+ error.DisplayedHelpMenu,
+ => {
+ stderr.buffered_writer.flush() catch {};
+ std.process.exit(1);
+ },
+ };
+}
+
+pub fn parseWithErrorWriter(
+ comptime p: Cli,
+ program_name: []const u8,
+ args: []const []const u8,
+ err: std.io.AnyWriter,
+ tty: std.io.tty.Config,
+ comptime show_help_on_error: bool,
+ comptime exit_on_error: bool,
+) ParseError!p.Parsed() {
+ var out: p.Parsed() = .{};
+ var had_errors = false;
+ var positionals_counted: usize = 0;
+ var first_positional: usize = 0;
+ var double_dash: ?usize = null;
+
+ var show_help = false;
+
+ const max_positionals = p.positionals.maxCount();
+
+ outer: for (args, 0..) |arg, i| {
+ if (mem.startsWith(u8, arg, "--")) {
+ if (arg.len == 2) {
+ if (max_positionals == 0) {
+ writeError(err, tty);
+ err.writeAll("unexpected end-of-parameter mark: --\n") catch {};
+ }
+
+ double_dash = i;
+ const remain = args[i + 1 ..];
+ if (positionals_counted == 0 and remain.len > 0)
+ first_positional = i + 1;
+
+ if (p.positionals == .bounded) {
+ out.positionals.appendSliceAssumeCapacity(remain[0..@min(remain.len, max_positionals -| positionals_counted)]);
+ }
+
+ positionals_counted += remain.len;
+
+ if (positionals_counted > max_positionals) {
+ had_errors = true;
+ const diff = @min(remain.len, positionals_counted - max_positionals);
+ for (remain[remain.len - diff ..]) |pos| {
+ writeError(err, tty);
+ err.print("unrecognized argument '{s}'\n", .{pos}) catch {};
+ }
+ }
+
+ break;
+ }
+
+ inline for (p.options) |def| try_this_def: {
+ const prefix = def.name;
+ const kind = def.kind;
+ const dest = &@field(out, fieldName(def.name));
+
+ const is_blank = arg.len == prefix.len;
+ if (!(mem.startsWith(u8, arg[2..], prefix[2..]) and (is_blank or arg[prefix.len] == '=')))
+ break :try_this_def;
+
+ if (kind != .boolean and is_blank) {
+ had_errors = true;
+ writeError(err, tty);
+ err.print(prefix ++ " requires a value\n", .{}) catch {};
+ continue :outer;
+ }
+
+ const value = if (is_blank) "" else arg[prefix.len + 1 ..];
+ switch (kind) {
+ .boolean => if (value.len == 0) {
+ dest.* = true;
+ } else if (mem.eql(u8, value, "false")) {
+ dest.* = false;
+ } else if (mem.eql(u8, value, "true")) {
+ dest.* = true;
+ } else {
+ had_errors = true;
+ writeError(err, tty);
+ err.print(prefix ++ " only accepts true or false, got '{s}'\n", .{
+ arg[prefix.len + 1 ..],
+ }) catch {};
+ },
- if (config.have_embedded_quickjs and run) {
- // Execute with QuickJS
- var out: std.ArrayListUnmanaged(u8) = .{};
- defer out.deinit(alloc);
- try js_backend.link(&comp, entry_point, out.writer(alloc).any());
- try out.append(alloc, 0);
- try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]);
+ .string => if (std.unicode.utf8ValidateSlice(value)) {
+ dest.* = value;
+ } else {
+ had_errors = true;
+ writeError(err, tty);
+ err.print(prefix ++ " requires valid UTF-8 input\n", .{}) catch {};
+ },
+
+ .@"enum" => |E| {
+ dest.* = std.meta.stringToEnum(E, value) orelse {
+ had_errors = true;
+ const values_list = comptime brk: {
+ var str: []const u8 = "";
+ for (std.enums.values(E)) |val| {
+ str = str ++ " " ++ @tagName(val) ++ "\n";
+ }
+ break :brk str;
+ };
+ writeError(err, tty);
+ err.print(prefix ++ " must be one of the following values, got {s}:\n{s}", .{
+ arg[prefix.len + 1 ..],
+ values_list,
+ }) catch {};
+ continue :outer;
+ };
+ },
+ }
+
+ continue :outer;
+ }
+
+ if (p.help.option) {
+ if (mem.startsWith(u8, arg[2..], "help") and (arg.len == "--help".len or arg["--help".len] == '=')) {
+ show_help = true;
+ if (arg.len > "--help".len) {
+ had_errors = true;
+ err.print("--help does not take a value\n", .{}) catch {};
+ }
+ continue :outer;
+ }
+ }
+
+ had_errors = true;
+ writeError(err, tty);
+ err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
+ } else if (arg.len > 1 and arg[0] == '-') {
+ per_char_in_short: for (arg[1..]) |ch| {
+ inline for (p.options) |def| try_this_def: {
+ const short = def.short orelse break :try_this_def;
+ if (def.kind != .boolean) {
+ @compileError("TODO: non-boolean shorthand arguments");
+ }
+ if (ch == short) {
+ const dest = &@field(out, fieldName(def.name));
+ dest.* = true;
+ continue :per_char_in_short;
+ }
+ if (p.help.option and ch == 'h') {
+ show_help = true;
+ }
+ }
+ }
} else {
- // Output to standard out
- const output = std.io.getStdOut().writer();
- var bufw = std.io.bufferedWriter(output);
- const writer = bufw.writer();
- try js_backend.link(&comp, entry_point, writer.any());
- try bufw.flush();
+ if (positionals_counted == 0) first_positional = i;
+ positionals_counted += 1;
+ if (positionals_counted > max_positionals) {
+ had_errors = true;
+ writeError(err, tty);
+ err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
+ } else if (p.positionals == .bounded) {
+ out.positionals.appendAssumeCapacity(arg);
+ }
}
}
- std.process.cleanExit();
- return 0;
+ if (had_errors) {
+ if (positionals_counted > max_positionals) {
+ writeNote(err, tty);
+ err.writeAll(switch (max_positionals) {
+ 0 => "this program does not take positional arguments\n",
+ 1 => "this program accepts one positional argument\n",
+ else => |n| std.fmt.comptimePrint(
+ "this program accepts up to {d} positional arguments\n",
+ .{n},
+ ),
+ }) catch {};
+ }
+ if (show_help or show_help_on_error) {
+ p.showHelpWithWriter(program_name, err, tty);
+ }
+ if (exit_on_error) {
+ return error.DisplayedHelpMenu;
+ }
+ return error.InvalidArguments;
+ }
+
+ if (show_help) {
+ p.showHelpWithWriter(program_name, err, tty);
+ return error.DisplayedHelpMenu;
+ }
+
+ out.positionals = switch (p.positionals) {
+ .none => {},
+ .single => if (positionals_counted > 0)
+ args[first_positional]
+ else
+ null,
+ .bounded => out.positionals, // already correct
+ .unlimited, .limited => if (max_positionals > 0) .{
+ .first = first_positional,
+ .len = positionals_counted,
+ .first_is_after_double_dash = if (double_dash) |dd|
+ dd < first_positional
+ else
+ false,
+ },
+ };
+
+ return out;
}
-fn runWithQuickJs(js: [:0]const u8) !void {
- const qjs = @import("quickjs");
-
- const runtime = qjs.JS_NewRuntime() orelse
- return error.UnexpectedError;
- defer qjs.JS_FreeRuntime(runtime);
- const js_ctx = qjs.JS_NewContext(runtime) orelse
- return error.UnexpectedError;
- defer qjs.JS_FreeContext(js_ctx);
-
- // QuickJS provides a completely blank global context. It is a non-goal to
- // be a full runtime, but adding simple globals to align with expectations
- // is OK.
-
- // 0 and null is zero process args
- qjs.js_std_add_helpers(js_ctx, 0, null);
-
- const result = qjs.JS_Eval(
- js_ctx,
- js.ptr,
- js.len,
- "test.js",
- qjs.JS_EVAL_TYPE_GLOBAL,
+pub fn showHelp(comptime p: Cli, program_name: []const u8) void {
+ var stderr = StdErrWriter.init();
+ defer stderr.buffered_writer.flush() catch {};
+ return showHelpWithWriter(
+ p,
+ program_name,
+ stderr.get(),
+ stderr.tty,
);
- defer qjs.JS_FreeValue(js_ctx, result);
+}
+
+pub fn showHelpWithWriter(
+ comptime p: Cli,
+ program_name: []const u8,
+ w: std.io.AnyWriter,
+ tty: std.io.tty.Config,
+) void {
+ const usage = p.help.usage orelse default: {
+ const opts_usage = if (p.options.len > 0 or p.help.option) "[options...]" else "";
+ break :default switch (p.positionals.maxCount()) {
+ 0 => opts_usage,
+ 1 => mem.trimLeft(u8, opts_usage ++ " [argument]", " "),
+ else => mem.trimLeft(u8, opts_usage ++ " [args...]", " "),
+ };
+ };
- if (qjs.JS_IsException(result) != 0) {
- const exception = qjs.JS_GetException(js_ctx);
- defer qjs.JS_FreeValue(js_ctx, exception);
+ w.print("usage: {s} {s}\n", .{ program_name, usage }) catch return;
- const exception_str = qjs.JS_ToCString(js_ctx, exception);
- defer qjs.JS_FreeCString(js_ctx, exception_str);
+ if (p.options.len > 0 or p.help.option) {
+ w.writeAll("\n") catch return;
+ tty.setColor(w, .bold) catch return;
+ w.writeAll("options:") catch return;
+ tty.setColor(w, .reset) catch return;
+ w.writeAll(comptime brk: {
+ var width = 0;
+ var any_short_options = false;
- const slice = std.mem.span(exception_str);
+ const all_opts = if (p.help.option)
+ p.options ++ [_]Option{.help}
+ else
+ p.options;
+ for (all_opts) |opt| {
+ width = @max(width, opt.name.len);
+ any_short_options = any_short_options or opt.short != null;
+ }
- std.debug.print("Exception: {s}\n", .{slice});
+ width += " ".len;
+ if (any_short_options) width += "-h ".len;
+ const sub = if (any_short_options) 4 else 1;
- std.process.exit(1);
+ var out: []const u8 = "\n";
+ for (all_opts) |opt| {
+ out = out ++ " ";
+ if (any_short_options) {
+ out = out ++ if (opt.short) |s| [_]u8{ '-', s, ' ' } else " ";
+ }
+ out = out ++ opt.name ++ (" " ** (width - opt.name.len - sub));
+ // TODO: wrapping at 72 columns
+ out = out ++ opt.desc ++ "\n";
+ }
+ break :brk out ++ "\n";
+ }) catch return;
}
}
-const core = @import("core");
-const Comp = core.Comp;
-const Report = core.Report;
+pub fn showHelpAndExit(comptime p: Cli, program_name: []const u8) noreturn {
+ p.showHelp(program_name);
+ std.process.exit(1);
+}
+
+fn writeError(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
+ tty.setColor(err, .red) catch return;
+ tty.setColor(err, .bold) catch return;
+ err.writeAll("error") catch return;
+ tty.setColor(err, .reset) catch return;
+ tty.setColor(err, .dim) catch return;
+ err.writeAll(": ") catch return;
+ tty.setColor(err, .reset) catch return;
+}
+
+fn writeNote(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
+ tty.setColor(err, .blue) catch return;
+ tty.setColor(err, .bold) catch return;
+ err.writeAll("note") catch return;
+ tty.setColor(err, .reset) catch return;
+ tty.setColor(err, .dim) catch return;
+ err.writeAll(": ") catch return;
+ tty.setColor(err, .reset) catch return;
+}
-const tracer = @import("tracer");
-pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));
+/// Splits args[0] containing the program name
+///
+/// const program_name, const args = programArgsAlloc();
+pub fn programArgsAlloc(allocator: std.mem.Allocator, fallback_name: []const u8) !struct { []const u8, [][:0]u8 } {
+ const magic_number = @alignOf([*][:0]u8);
+ var raw_args = try std.process.argsAlloc(allocator);
+ switch (raw_args.len) {
+ 0 => {
+ raw_args.ptr = @ptrFromInt(magic_number);
+ return .{ fallback_name, raw_args };
+ },
+ else => return .{
+ std.fs.path.basename(raw_args[0]),
+ raw_args[1..],
+ },
+ }
+}
+
+pub fn programArgsFree(allocator: std.mem.Allocator, args: [][:0]u8) void {
+ const magic_number = @alignOf([*][:0]u8);
+ if (args.len == 0 and @intFromPtr(args.ptr) == magic_number) return;
+ const raw_args = (args.ptr - 1)[0 .. args.len + 1];
+ std.process.argsFree(allocator, raw_args);
+}
+
+pub fn fatalWithHelp(p: Cli, program_name: []const u8, comptime fmt: []const u8, args: anytype) noreturn {
+ print: {
+ const stderr = std.io.getStdErr();
+ const tty = std.io.tty.detectConfig(stderr);
+ var bufw = std.io.bufferedWriter(stderr.writer());
+ writeError(bufw.writer().any(), tty);
+ bufw.writer().print(fmt ++ "\n", args) catch break :print;
+ p.showHelpWithWriter(program_name, bufw.writer().any(), tty);
+ bufw.flush() catch break :print;
+ }
+ std.process.exit(1);
+}
+
+pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
+ const stderr = std.io.getStdErr();
+ const tty = std.io.tty.detectConfig(stderr);
+ var bufw = std.io.bufferedWriter(stderr.writer());
+ writeError(bufw.writer().any(), tty);
+ bufw.writer().print(fmt ++ "\n", args);
+ bufw.flush() catch {}; // ignoring I/O errors
+ std.process.exit(1);
+}
+
+pub fn main() !void {
+ var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
+ defer _ = gpa.deinit();
+ const alloc = gpa.allocator();
+
+ const config: Cli = .{
+ .options = &.{
+ .{ .name = "--flag", .desc = "check for something" },
+ .{ .name = "--str", .desc = "pass a string", .kind = .string },
+ .{
+ .name = "--enum",
+ .desc = "enum",
+ .kind = .{ .@"enum" = std.builtin.OptimizeMode },
+ },
+ },
+ .positionals = .{ .bounded = 2 },
+ };
+
+ const program_name, const args = try Cli.programArgsAlloc(alloc);
+ defer Cli.programArgsFree(alloc, args);
+
+ const parsed = config.parse(args[1..]) catch
+ config.showHelpAndExit(program_name);
+
+ std.debug.print("--flag: {any}\n", .{parsed.flag});
+ std.debug.print("--str: {?s}\n", .{parsed.str});
+ std.debug.print("--enum: {any}\n", .{parsed.@"enum"});
+ std.debug.print("positionals({d}):\n", .{parsed.positionals.len});
+
+ // if using '.positionals = .unlimited'
+ // var it = parsed.positionals.iterator(args[1..]);
+ // while (it.next()) |item| {
+ // std.debug.print(" - {s}\n", .{item});
+ // }
+
+ for (parsed.positionals.slice()) |item| {
+ std.debug.print(" - {s}\n", .{item});
+ }
+}
+const Cli = @This();
const std = @import("std");
-const builtin = @import("builtin");
-
-fn dumpState(comp: *const Comp, dump_path: []const u8) !void {
- const file = try std.fs.cwd().createFile(dump_path, .{});
- defer file.close();
-
- var bufw = std.io.bufferedWriter(file.writer());
- var jw = std.json.writeStream(bufw.writer(), .{
- .whitespace = .indent_2,
- });
-
- try comp.jsonStringify(&jw);
- try bufw.writer().writeAll("\n");
- try bufw.flush();
-}
+const mem = std.mem;
+const Type = std.builtin.Type;
diff --git a/src/cli_transform.zig b/src/cli_transform.zig
new file mode 100644
index 0000000000000000000000000000000000000000..44920293e9b2d00e4fbde53476d3b694c332b957
--- /dev/null
+++ b/src/cli_transform.zig
@@ -0,0 +1,98 @@
+//! Minimal CLI via `zig build transform -- filepath`
+//! Use this only for working on Parse+Transform
+
+const arg_config: Cli = .{
+ .options = &.{
+ .{ .name = "--ast", .desc = "Show AST", .short = 'a' },
+ .{ .name = "--dump-bin", .kind = .string, .desc = "Dump serialized binary to file" },
+ },
+ .positionals = .single,
+};
+
+pub fn main() !u8 {
+ try core.unicode_props.init();
+
+ const use_gpa = @import("builtin").mode == .Debug;
+ var gpa = if (use_gpa)
+ std.heap.GeneralPurposeAllocator(.{
+ .stack_trace_frames = 100,
+ }){}
+ else
+ std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ defer _ = if (use_gpa) gpa.deinit();
+ const alloc = gpa.allocator();
+
+ const program_name, const raw_args = try Cli.programArgsAlloc(alloc, "transform");
+ defer Cli.programArgsFree(alloc, raw_args);
+
+ const args = arg_config.parse(raw_args) catch
+ arg_config.showHelpAndExit(program_name);
+
+ const file_path = args.positionals orelse
+ arg_config.fatalWithHelp(program_name, "missing filepath argument\n", .{});
+
+ const out = core.OutStream.getStderr();
+
+ const code = try std.fs.cwd()
+ .readFileAllocOptions(alloc, file_path, std.math.maxInt(u32), null, @alignOf(u8), 0);
+ defer alloc.free(code);
+
+ var err_data: core.Parse.AstError.WithTrace = undefined;
+ var ast = core.Parse.parse(
+ alloc,
+ code,
+ &err_data,
+ ) catch |err| switch (err) {
+ error.OutOfMemory => return err,
+ error.ParseFail => {
+ var report = try err_data.toReport(alloc);
+ defer report.deinit(alloc);
+
+ report.dump(
+ .{ .single_file = .{
+ .allocator = alloc,
+ .url = file_path,
+ .file = .{ .utf8_source = code },
+ } },
+ out,
+ ) catch {};
+
+ return 1;
+ },
+ };
+ defer ast.deinit(alloc);
+
+ if (args.ast == true) {
+ ast.dumpToWriter(out.writer) catch {};
+ }
+
+ var teg_report: core.Report = undefined;
+ var teg = core.Transform.transform(ast, alloc, &teg_report) catch |err| switch (err) {
+ error.OutOfMemory => return err,
+ error.TransformFail => {
+ defer teg_report.deinit(alloc);
+ teg_report.dump(
+ .{ .single_file = .{
+ .allocator = alloc,
+ .url = file_path,
+ .file = .{ .ast = &ast },
+ } },
+ out,
+ ) catch {};
+ return 1;
+ },
+ };
+ defer teg.deinit(alloc);
+
+ // const file = try std.fs.cwd().createFile("serialized.teg", .{});
+ // defer file.close();
+ // try teg.serialize(.{ .file = file });
+
+ try teg.dumpToWriter(out.writer, out.tty);
+
+ return 0;
+}
+
+const std = @import("std");
+const core = @import("core");
+const Cli = @import("Cli.zig");
diff --git a/src/core/transform_cli.zig b/src/core/transform_cli.zig
deleted file mode 100644
index ecbe0960b6f0048f2ffd783c6b6d23ce73336de9..0000000000000000000000000000000000000000
--- a/src/core/transform_cli.zig
+++ /dev/null
@@ -1,85 +0,0 @@
-//! Minimal CLI via `zig build transform -- filepath`
-//! Use this only for working on Parse+Transform
-
-pub fn main() !u8 {
- try core.unicode_props.init();
-
- const use_gpa = @import("builtin").mode == .Debug;
- var gpa = if (use_gpa)
- std.heap.GeneralPurposeAllocator(.{
- .stack_trace_frames = 100,
- }){}
- else
- std.heap.ArenaAllocator.init(std.heap.page_allocator);
- defer _ = if (use_gpa) gpa.deinit();
- const alloc = gpa.allocator();
-
- var args = try std.process.argsWithAllocator(alloc);
- defer args.deinit();
-
- _ = args.skip();
- const file_path = args.next() orelse {
- std.debug.print("Missing filepath argument\n", .{});
- return 1;
- };
-
- const out = core.OutStream.getStderr();
-
- const code = try std.fs.cwd()
- .readFileAllocOptions(alloc, file_path, std.math.maxInt(u32), null, @alignOf(u8), 0);
- defer alloc.free(code);
-
- var err_data: core.Parse.AstError.WithTrace = undefined;
- var ast = core.Parse.parse(
- alloc,
- code,
- &err_data,
- ) catch |err| switch (err) {
- error.OutOfMemory => return err,
- error.ParseFail => {
- var report = try err_data.toReport(alloc);
- defer report.deinit(alloc);
-
- report.dump(
- .{ .single_file = .{
- .allocator = alloc,
- .url = file_path,
- .file = .{ .utf8_source = code },
- } },
- out,
- ) catch {};
-
- return 1;
- },
- };
- defer ast.deinit(alloc);
-
- var teg_report: core.Report = undefined;
- var teg = core.Transform.transform(ast, alloc, &teg_report) catch |err| switch (err) {
- error.OutOfMemory => return err,
- error.TransformFail => {
- defer teg_report.deinit(alloc);
- teg_report.dump(
- .{ .single_file = .{
- .allocator = alloc,
- .url = file_path,
- .file = .{ .ast = &ast },
- } },
- out,
- ) catch {};
- return 1;
- },
- };
- defer teg.deinit(alloc);
-
- // const file = try std.fs.cwd().createFile("serialized.teg", .{});
- // defer file.close();
- // try teg.serialize(.{ .file = file });
-
- try teg.dumpToWriter(out.writer, out.tty);
-
- return 0;
-}
-
-const std = @import("std");
-const core = @import("core");
diff --git a/src/main.zig b/src/main.zig
new file mode 100644
index 0000000000000000000000000000000000000000..ebbe1e8800327ce7b1a243bec9b8b4780c79dd1d
--- /dev/null
+++ b/src/main.zig
@@ -0,0 +1,260 @@
+//! This is the `lang` command line interface. It combines all components
+//! of the software development kit into one. Multiple versions of this
+//! file are built with different subsets of features, allowing you to,
+//! for example, test the `core` module's compilation features without
+//! needing to compile the formatter.
+
+const have_debug_features = builtin.mode == .Debug;
+const config = @import("config");
+
+pub fn main() !u8 {
+ try tracer.init();
+ defer tracer.deinit();
+ try tracer.init_thread(null);
+ defer tracer.deinit_thread();
+
+ // unicode props is asserted to be initialized before core.Tokenizer is used
+ // this will result in a static page allocation, which is never cleaned up.
+ try core.unicode_props.init();
+
+ const use_gpa = builtin.mode == .Debug;
+ var gpa = if (use_gpa)
+ std.heap.GeneralPurposeAllocator(.{
+ .stack_trace_frames = 100,
+ }){}
+ else
+ std.heap.ArenaAllocator.init(std.heap.page_allocator);
+ var no_leak_checks = false;
+ defer _ = if (use_gpa and !no_leak_checks) gpa.deinit();
+ const alloc = gpa.allocator();
+
+ // Current working directory is required for CLI resolution.
+ var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const cwd = try std.posix.getcwd(&cwd_buf);
+
+ // Command line argument parsing.
+ var maybe_file_path: ?[]const u8 = null;
+ defer if (maybe_file_path) |path| alloc.free(path);
+ var exe_name: ?[]const u8 = null;
+ defer if (exe_name) |path| alloc.free(path);
+ var dump_ir = false;
+ var dump_state: ?[]const u8 = null;
+ var run = false;
+ var has_errors = false;
+ {
+ var args = try std.process.argsWithAllocator(alloc);
+ defer args.deinit();
+
+ exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli"));
+
+ while (args.next()) |arg| {
+ if (std.mem.startsWith(u8, arg, "-")) {
+ if (have_debug_features) {
+ if (std.mem.eql(u8, arg, "--report-trace")) {
+ core.Report.debug_show_report_trace = true;
+ continue;
+ }
+ if (std.mem.eql(u8, arg, "--no-leak-checks")) {
+ no_leak_checks = true;
+ continue;
+ }
+ }
+
+ if (std.mem.eql(u8, arg, "--dump-ir")) {
+ dump_ir = true;
+ continue;
+ }
+
+ if (std.mem.startsWith(u8, arg, "--dump-state=")) {
+ dump_state = arg["--dump-state=".len..];
+ continue;
+ }
+
+ if (config.have_embedded_quickjs and std.mem.eql(u8, arg, "--run")) {
+ run = true;
+ continue;
+ }
+
+ std.debug.print("error: unknown flag '{s}'\n", .{arg});
+ has_errors = true;
+ break;
+ }
+
+ if (maybe_file_path == null) {
+ maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg });
+ } else {
+ std.debug.print("error: multiple files specified\n", .{});
+ maybe_file_path = null;
+ break;
+ }
+ }
+ }
+
+ if (maybe_file_path == null) has_errors = true;
+ if (has_errors) {
+ std.debug.print("Usage: {s} [flags] \n", .{exe_name.?});
+ {
+ std.debug.print(
+ // | 60 chars wrap point|
+ \\
+ \\Flags:
+ \\ --dump-ir Dump intermediate representations
+ \\ --dump-state= Dump compiler state to JSON
+ \\
+ , .{});
+ }
+ if (config.have_embedded_quickjs) {
+ std.debug.print(
+ \\ --run Run program using QuickJS
+ \\
+ , .{});
+ }
+ if (have_debug_features) {
+ std.debug.print(
+ \\
+ \\Debug Mode Flags:
+ \\ --report-trace Print stack traces per error
+ \\ --no-leak-checks
+ \\
+ , .{});
+ }
+ std.debug.print("\nLanguage SDK v" ++ config.version_string ++ "\n", .{});
+ return 1;
+ }
+ const file_path = maybe_file_path.?;
+
+ // Execute compiler
+ {
+ var js_backend = core.Js{};
+
+ var comp = try core.Comp.init(.{
+ .gpa = tracer.tracingAllocator(&alloc),
+ .options = .{
+ .dump_ir = dump_ir,
+ },
+ .backend = .{ .js = &js_backend },
+ });
+ defer comp.deinit();
+
+ defer if (dump_state) |path| {
+ dumpState(&comp, path) catch |err| {
+ std.debug.print("Failed to dump compilation state: {}", .{err});
+ };
+ };
+
+ const main_file = try comp.getFile(file_path);
+
+ var fail_report: Report = undefined;
+ const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) {
+ error.TransformFail => {
+ std.debug.print("Transform failed:\n", .{});
+ try fail_report.dump(.initComp(&comp, main_file), comp.out);
+ fail_report.deinit(comp.gpa);
+ return 1;
+ },
+ error.ParseFail => {
+ // Already printed.
+ return 1;
+ },
+ else => return err,
+ };
+
+ if (comp.options.dump_ir) {
+ const stderr = std.io.getStdErr().writer();
+ try teg.dumpToWriter(stderr, null);
+ }
+
+ const entry_point = try comp.enqueueEntryModule(main_file);
+ comp.runAllJobs() catch |err| switch (err) {
+ error.AnalysisFail => {
+ // error is already printed
+ return 1;
+ },
+ else => return err,
+ };
+
+ if (config.have_embedded_quickjs and run) {
+ // Execute with QuickJS
+ var out: std.ArrayListUnmanaged(u8) = .{};
+ defer out.deinit(alloc);
+ try js_backend.link(&comp, entry_point, out.writer(alloc).any());
+ try out.append(alloc, 0);
+ try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]);
+ } else {
+ // Output to standard out
+ const output = std.io.getStdOut().writer();
+ var bufw = std.io.bufferedWriter(output);
+ const writer = bufw.writer();
+ try js_backend.link(&comp, entry_point, writer.any());
+ try bufw.flush();
+ }
+ }
+
+ std.process.cleanExit();
+ return 0;
+}
+
+fn runWithQuickJs(js: [:0]const u8) !void {
+ const qjs = @import("quickjs");
+
+ const runtime = qjs.JS_NewRuntime() orelse
+ return error.UnexpectedError;
+ defer qjs.JS_FreeRuntime(runtime);
+ const js_ctx = qjs.JS_NewContext(runtime) orelse
+ return error.UnexpectedError;
+ defer qjs.JS_FreeContext(js_ctx);
+
+ // QuickJS provides a completely blank global context. It is a non-goal to
+ // be a full runtime, but adding simple globals to align with expectations
+ // is OK.
+
+ // 0 and null is zero process args
+ qjs.js_std_add_helpers(js_ctx, 0, null);
+
+ const result = qjs.JS_Eval(
+ js_ctx,
+ js.ptr,
+ js.len,
+ "test.js",
+ qjs.JS_EVAL_TYPE_GLOBAL,
+ );
+ defer qjs.JS_FreeValue(js_ctx, result);
+
+ if (qjs.JS_IsException(result) != 0) {
+ const exception = qjs.JS_GetException(js_ctx);
+ defer qjs.JS_FreeValue(js_ctx, exception);
+
+ const exception_str = qjs.JS_ToCString(js_ctx, exception);
+ defer qjs.JS_FreeCString(js_ctx, exception_str);
+
+ const slice = std.mem.span(exception_str);
+
+ std.debug.print("Exception: {s}\n", .{slice});
+
+ std.process.exit(1);
+ }
+}
+
+const core = @import("core");
+const Comp = core.Comp;
+const Report = core.Report;
+
+const tracer = @import("tracer");
+pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));
+
+const std = @import("std");
+const builtin = @import("builtin");
+
+fn dumpState(comp: *const Comp, dump_path: []const u8) !void {
+ const file = try std.fs.cwd().createFile(dump_path, .{});
+ defer file.close();
+
+ var bufw = std.io.bufferedWriter(file.writer());
+ var jw = std.json.writeStream(bufw.writer(), .{
+ .whitespace = .indent_2,
+ });
+
+ try comp.jsonStringify(&jw);
+ try bufw.writer().writeAll("\n");
+ try bufw.flush();
+}
diff --git a/test/compiler.zig b/test/compiler.zig
index 9fca14512ef06db4f5f8a9d836221c2a3a077fa5..d5b1cd5ca6f55e1142232dcf922325538513cf1f 100644
--- a/test/compiler.zig
+++ b/test/compiler.zig
@@ -5,6 +5,19 @@
//! structure of these tests, and `zig build test-compiler -- --help` for more
//! details on the arguments this supports.
+const cli_config: Cli = .{
+ .options = &.{
+ .{ .name = "--test-root", .desc = "root directory of all tests", .kind = .string },
+ .{ .name = "--all-logs", .desc = "Always print compiler logs" },
+ .{ .name = "--no-threads", .desc = "Avoid using a thread pool" },
+ .{ .name = "--dump-ir", .desc = "Dump intermediate representations" },
+ },
+ .positionals = .single,
+ .help = .{
+ .usage = "[options...] [test filter]",
+ },
+};
+
/// This file is included for all behavior tests for the JavaScript backend.
const helpers =
\\(function (self) {
@@ -237,50 +250,19 @@ pub fn main() !void {
defer _ = gpa.deinit();
const alloc = gpa.allocator();
- const args = try std.process.argsAlloc(alloc);
- defer std.process.argsFree(alloc, args);
-
- var test_root: []const u8 = "test/behavior";
- var case_options: CaseOptions = .{};
- var all_logs = false;
- var filter: ?[]const u8 = null;
- defer if (filter) |f| alloc.free(f);
-
- var argument_error = false;
-
- for (args[1..]) |full_arg| {
- if (std.mem.startsWith(u8, full_arg, "--")) {
- if (getFlag("--test-root", full_arg)) |flag| {
- test_root = flag;
- } else if (std.mem.eql(u8, "--dump-ir", full_arg)) {
- case_options.dump_ir = true;
- } else if (std.mem.eql(u8, "--all-logs", full_arg)) {
- all_logs = true;
- } else if (std.mem.eql(u8, "--sync", full_arg)) {
- case_options.sync = true;
- } else {
- std.debug.print("Unknown argument: {s}\n", .{full_arg});
- argument_error = true;
- }
- } else {
- if (filter != null) {
- std.debug.print("Unknown positional argument: {s}\n", .{full_arg});
- } else {
- filter = try std.mem.replaceOwned(
- u8,
- alloc,
- full_arg,
- if (std.fs.path.sep == '/') "\\" else '/',
- std.fs.path.sep_str,
- );
- }
- }
- }
-
- if (argument_error) {
- // TODO: print a nice help menu
- std.process.exit(1);
- }
+ const program_name = "zig build test-compiler --";
+ _, const args = try Cli.programArgsAlloc(alloc, "");
+ defer Cli.programArgsFree(alloc, args);
+
+ const opts = cli_config.parse(program_name, args);
+
+ const test_root = opts.test_root orelse "test/behavior";
+ const case_options: CaseOptions = .{
+ .dump_ir = opts.dump_ir orelse false,
+ .sync = opts.no_threads orelse false,
+ };
+ const all_logs = opts.all_logs orelse false;
+ const filter: ?[]const u8 = opts.positionals;
const some_logs = all_logs or args.len > 1;
@@ -300,7 +282,7 @@ pub fn main() !void {
if (filter) |f| {
std.debug.print("No test cases match filter \"{s}\"\n", .{f});
} else {
- std.debug.print("No test cases\n", .{});
+ std.debug.print("No test cases in root \"{s}\"", .{test_root});
}
std.process.exit(1);
}
@@ -424,6 +406,7 @@ fn getFlag(name: []const u8, full_arg: []const u8) ?[]const u8 {
const std = @import("std");
const Dir = std.fs.Dir;
+const Cli = @import("Cli");
const core = @import("core");
const qjs = @import("quickjs");