From 03d5aa6a575bcd5237473b8f87d273504e6ed4b9 Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 14 Sep 2024 01:47:20 -0400 Subject: [PATCH] chore: write a proper CLI parser utility intention later will be to separate 'main.zig' into itself and 'cli_compiler.zig' with the latter being the minimal dev-focused binary. the thing built for release would be 'main.zig' --- build.zig | 40 +- src/cli.zig | 834 +++++++++++++----- .../transform_cli.zig => cli_transform.zig} | 27 +- src/main.zig | 260 ++++++ test/compiler.zig | 73 +- 5 files changed, 957 insertions(+), 277 deletions(-) rename src/{core/transform_cli.zig => cli_transform.zig} (75%) create mode 100644 src/main.zig 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/core/transform_cli.zig b/src/cli_transform.zig similarity index 75% rename from src/core/transform_cli.zig rename to src/cli_transform.zig index ecbe0960b6f0048f2ffd783c6b6d23ce73336de9..44920293e9b2d00e4fbde53476d3b694c332b957 100644 --- a/src/core/transform_cli.zig +++ b/src/cli_transform.zig @@ -1,6 +1,14 @@ //! 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(); @@ -14,14 +22,14 @@ pub fn main() !u8 { defer _ = if (use_gpa) gpa.deinit(); const alloc = gpa.allocator(); - var args = try std.process.argsWithAllocator(alloc); - defer args.deinit(); + const program_name, const raw_args = try Cli.programArgsAlloc(alloc, "transform"); + defer Cli.programArgsFree(alloc, raw_args); - _ = args.skip(); - const file_path = args.next() orelse { - std.debug.print("Missing filepath argument\n", .{}); - return 1; - }; + 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(); @@ -54,6 +62,10 @@ pub fn main() !u8 { }; 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, @@ -83,3 +95,4 @@ pub fn main() !u8 { const std = @import("std"); const core = @import("core"); +const Cli = @import("Cli.zig"); 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"); -- 2.54.0