| ... | ... | @@ -1,31 +1,155 @@ |
| 1 | | //! This is the `lang` command line interface. It combines all components |
| 2 | | //! of the software development kit into one. Multiple versions of this |
| 3 | | //! file are built with different subsets of features, allowing you to, |
| 4 | | //! for example, test the `core` module's compilation features without |
| 5 | | //! needing to compile the formatter. |
| 1 | //! This is the user-facing command line, combining all features into |
| 2 | //! different subcommands, each with their own option set. |
| 6 | 3 | |
| 7 | | const config = @import("config"); |
| 4 | const default_program_name = "wlang"; // fallback when argv[0] is missing |
| 8 | 5 | |
| 9 | | const arg_config: Cli = .{ |
| 10 | | .options = .{ |
| 11 | | .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" }, |
| 12 | | .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string }, |
| 13 | | } ++ Cli.Option.optional(config.have_embedded_quickjs, &.{ |
| 14 | | .{ .name = "--run", .desc = "Execute with QuickJS" }, |
| 6 | const Command = struct { |
| 7 | name: [:0]const u8, |
| 8 | description: []const u8, |
| 9 | cli: Cli, |
| 10 | run: fn (Context) anyerror!void, |
| 11 | enabled: bool = true, |
| 12 | |
| 13 | const ListItem = union(enum) { |
| 14 | separator, |
| 15 | command: Command, |
| 16 | |
| 17 | fn cmd(command: Command) ListItem { |
| 18 | return .{ .command = command }; |
| 19 | } |
| 20 | }; |
| 21 | }; |
| 22 | |
| 23 | const general_compiler_options: []const Cli.Option = &.{ |
| 24 | .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" }, |
| 25 | .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string }, |
| 26 | }; |
| 27 | |
| 28 | const top_level_commands: []const Command.ListItem = &.{ |
| 29 | // Compilation |
| 30 | .cmd(.{ |
| 31 | .name = "build", |
| 32 | .description = "Create an executable from source code", |
| 33 | .cli = .{ |
| 34 | .options = general_compiler_options, |
| 35 | .positionals = .single, |
| 36 | .help = .{ .usage = "[options...] [entry file]" }, |
| 37 | }, |
| 38 | .run = cmdBuild, |
| 39 | }), |
| 40 | .cmd(.{ |
| 41 | .name = "run", |
| 42 | .description = "Create executable and run with QuickJS", |
| 43 | .cli = .{ |
| 44 | .options = general_compiler_options ++ .{ |
| 45 | .{ .name = "--dump-code", .desc = "Dump compiled JavaScript to standard out" }, |
| 46 | }, |
| 47 | .positionals = .single, |
| 48 | .help = .{ .usage = "[options...] [entry file]" }, |
| 49 | }, |
| 50 | .enabled = config.have_embedded_quickjs, |
| 51 | .run = cmdRun, |
| 52 | }), |
| 53 | |
| 54 | // Utilities |
| 55 | // .separator, |
| 56 | // .cmd(.{ |
| 57 | // .name = "fmt", |
| 58 | // .description = "Reformat code into canonical form", |
| 59 | // .cli = .{ |
| 60 | // .options = &.{}, |
| 61 | // .positionals = .unlimited, |
| 62 | // .help = .{ .usage = "[options...] [files...]" }, |
| 63 | // }, |
| 64 | // .run = cmdFmt, |
| 65 | // }), |
| 66 | |
| 67 | // Statistics |
| 68 | .separator, |
| 69 | .cmd(.{ |
| 70 | .name = "version", |
| 71 | .description = "Print version number and exit", |
| 72 | .cli = .empty, |
| 73 | .run = undefined, // special cased |
| 74 | }), |
| 75 | .cmd(.{ |
| 76 | .name = "help", |
| 77 | .description = "Print this help and exit", |
| 78 | .cli = .empty, |
| 79 | .run = undefined, // special cased |
| 15 | 80 | }), |
| 16 | | .positionals = .single, |
| 17 | 81 | }; |
| 18 | 82 | |
| 19 | | pub fn main() !u8 { |
| 83 | const Context = struct { |
| 84 | gpa: Allocator, |
| 85 | cwd: []const u8, |
| 86 | /// argv[0] ++ " " ++ subcommand.name |
| 87 | program_name: []const u8, |
| 88 | |
| 89 | /// Type-erasure is needed so `top_level_commands` can hold a non-generic |
| 90 | /// function, as it is not possible to have it's parameter type depend on a |
| 91 | /// sibling field. Retrieve its concrete type via `.options()`. |
| 92 | raw_options: *align(8) const anyopaque, |
| 93 | /// This cannot be `SubCommand` directly as it would cause a dependency loop. |
| 94 | safety_check: if (core.runtime_safety) u8 else void, |
| 95 | |
| 96 | pub fn options(ctx: Context, comptime id: CommandId) ParsedOptions(id) { |
| 97 | assert(@intFromEnum(id) == ctx.safety_check); // incorrect memory access |
| 98 | return @as(*const ParsedOptions(id), @ptrCast(ctx.raw_options)).*; |
| 99 | } |
| 100 | |
| 101 | pub fn showHelp(ctx: Context, comptime id: CommandId) void { |
| 102 | getSubCommand(id).cli.showHelp(ctx.program_name); |
| 103 | } |
| 104 | }; |
| 105 | |
| 106 | const CommandId = @Type(.{ .@"enum" = .{ |
| 107 | .tag_type = u8, |
| 108 | .fields = fields: { |
| 109 | var fields: [top_level_commands.len]std.builtin.Type.EnumField = undefined; |
| 110 | var i: usize = 0; |
| 111 | for (top_level_commands) |tlc| switch (tlc) { |
| 112 | .separator => continue, |
| 113 | .command => |cmd| if (cmd.enabled) { |
| 114 | fields[i] = .{ |
| 115 | .name = cmd.name, |
| 116 | .value = i, |
| 117 | }; |
| 118 | i += 1; |
| 119 | }, |
| 120 | }; |
| 121 | break :fields &(fields[0..i].*); |
| 122 | }, |
| 123 | .decls = &.{}, |
| 124 | .is_exhaustive = true, |
| 125 | } }); |
| 126 | |
| 127 | fn getSubCommand(subcommand: CommandId) Command { |
| 128 | return for (top_level_commands) |tlc| switch (tlc) { |
| 129 | .separator => continue, |
| 130 | .command => |cmd| if (std.mem.eql(u8, cmd.name, @tagName(subcommand))) |
| 131 | break cmd, |
| 132 | } else unreachable; |
| 133 | } |
| 134 | |
| 135 | fn ParsedOptions(subcommand: CommandId) type { |
| 136 | return getSubCommand(subcommand).cli.Parsed(); |
| 137 | } |
| 138 | |
| 139 | pub fn main() !void { |
| 20 | 140 | try tracer.init(); |
| 21 | 141 | defer tracer.deinit(); |
| 22 | 142 | try tracer.init_thread(null); |
| 23 | 143 | defer tracer.deinit_thread(); |
| 24 | 144 | |
| 25 | | // unicode props is asserted to be initialized before core.Tokenizer is used |
| 26 | | // this will result in a static page allocation, which is never cleaned up. |
| 145 | // 'unicode_props' is asserted to be initialized when core.Tokenizer is used. |
| 27 | 146 | try core.unicode_props.init(); |
| 147 | defer core.unicode_props.deinit(); |
| 28 | 148 | |
| 149 | // General Purpose Allocator provides great debugging assertions, but |
| 150 | // in Zig 0.14 it is unsuitible for release builds. To get reasonable |
| 151 | // performance, an arena over the page allocator is used. |
| 152 | // TODO: investigate other strategies, such as mi-malloc |
| 29 | 153 | const use_gpa = builtin.mode == .Debug; |
| 30 | 154 | var gpa = if (use_gpa) |
| 31 | 155 | std.heap.GeneralPurposeAllocator(.{ |
| ... | ... | @@ -38,18 +162,118 @@ pub fn main() !u8 { |
| 38 | 162 | |
| 39 | 163 | // Current working directory is required for CLI resolution. |
| 40 | 164 | var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; |
| 41 | | const cwd = try std.posix.getcwd(&cwd_buf); |
| 165 | const cwd = std.posix.getcwd(&cwd_buf) catch |err| { |
| 166 | _ = @errorReturnTrace(); // TODO |
| 167 | Cli.fatal("Failed to query current working directory: {s}", .{@errorName(err)}); |
| 168 | }; |
| 42 | 169 | |
| 43 | 170 | // Command line argument parsing. |
| 44 | | const program_name = "zig build compiler --"; |
| 45 | | _, const args = try Cli.programArgsAlloc(alloc, program_name); |
| 171 | const program_base_name, const args = try Cli.programArgsAlloc(alloc, default_program_name); |
| 46 | 172 | defer Cli.programArgsFree(alloc, args); |
| 47 | | const opts = arg_config.parse(program_name, args); |
| 173 | |
| 174 | var joined_program_name: [512]u8 = undefined; |
| 175 | @memcpy(joined_program_name[0..program_base_name.len], program_base_name); |
| 176 | |
| 177 | const stderr = OutStream.getStderr(); |
| 178 | |
| 179 | if (args.len == 0) { |
| 180 | try showTopLevelHelp(stderr, program_base_name); |
| 181 | Cli.fatal("expected command argument", .{}); |
| 182 | } |
| 183 | |
| 184 | const command = std.meta.stringToEnum(CommandId, args[0]) orelse { |
| 185 | try showTopLevelHelp(stderr, program_base_name); |
| 186 | if (std.mem.eql(u8, args[0], "--help")) { |
| 187 | std.process.cleanExit(); |
| 188 | return; |
| 189 | } |
| 190 | |
| 191 | inline for (top_level_commands) |tlc| { |
| 192 | if (tlc == .command and !tlc.command.enabled) { |
| 193 | if (std.mem.eql(u8, args[0], tlc.command.name)) { |
| 194 | Cli.fatal("this build configuration does not include the {s} command", .{ |
| 195 | tlc.command.name, |
| 196 | }); |
| 197 | } |
| 198 | } |
| 199 | } |
| 200 | |
| 201 | Cli.fatal("unknown command '{s}'", .{args[0]}); |
| 202 | }; |
| 203 | |
| 204 | switch (command) { |
| 205 | // Help and version ignore all passed arguments |
| 206 | .help => { |
| 207 | try showTopLevelHelp(stderr, program_base_name); |
| 208 | std.process.cleanExit(); |
| 209 | }, |
| 210 | .version => { |
| 211 | const stdout = std.io.getStdOut(); |
| 212 | try stdout.writeAll(config.version_string ++ "\n"); |
| 213 | std.process.cleanExit(); |
| 214 | }, |
| 215 | inline else => |cmd| { |
| 216 | const def = comptime getSubCommand(cmd); |
| 217 | joined_program_name[program_base_name.len] = ' '; |
| 218 | @memcpy( |
| 219 | joined_program_name[program_base_name.len + 1 ..][0..def.name.len], |
| 220 | def.name, |
| 221 | ); |
| 222 | const name = joined_program_name[0 .. program_base_name.len + def.name.len + 1]; |
| 223 | const parsed = def.cli.parse(name, args[1..]); |
| 224 | try def.run(.{ |
| 225 | .cwd = cwd, |
| 226 | .gpa = alloc, |
| 227 | .raw_options = &parsed, |
| 228 | .safety_check = @intFromEnum(cmd), |
| 229 | .program_name = name, |
| 230 | }); |
| 231 | }, |
| 232 | } |
| 233 | |
| 234 | std.process.cleanExit(); |
| 235 | } |
| 236 | |
| 237 | fn showTopLevelHelp(out: OutStream, program_name: []const u8) !void { |
| 238 | try out.setColor(.bold); |
| 239 | try out.print("usage", .{}); |
| 240 | try out.setColor(.reset); |
| 241 | try out.print(": {s} [command] [options...]\n\n", .{program_name}); |
| 242 | try out.setColor(.bold); |
| 243 | try out.print("commands", .{}); |
| 244 | try out.setColor(.reset); |
| 245 | try out.writer.writeAll(comptime rest: { |
| 246 | var name_width: usize = 0; |
| 247 | for (top_level_commands) |tlc| switch (tlc) { |
| 248 | .separator => continue, |
| 249 | .command => |cmd| if (cmd.enabled) { |
| 250 | name_width = @max(cmd.name.len, name_width); |
| 251 | }, |
| 252 | }; |
| 253 | name_width += 4; |
| 254 | var rest: []const u8 = ":\n\n"; |
| 255 | for (top_level_commands) |tlc| switch (tlc) { |
| 256 | .separator => rest = rest ++ "\n", |
| 257 | .command => |cmd| if (cmd.enabled) { |
| 258 | rest = rest ++ " " ++ |
| 259 | cmd.name ++ (" " ** (name_width - cmd.name.len)) ++ |
| 260 | cmd.description ++ "\n"; |
| 261 | }, |
| 262 | }; |
| 263 | break :rest rest ++ "\n"; |
| 264 | }); |
| 265 | } |
| 266 | |
| 267 | fn cmdBuildOrRun(ctx: Context, comptime mode: enum { build, run }) !void { |
| 268 | const opts = ctx.options(@field(CommandId, @tagName(mode))); |
| 269 | const alloc = ctx.gpa; |
| 48 | 270 | |
| 49 | 271 | const file_path = try std.fs.path.resolve(alloc, &.{ |
| 50 | | cwd, |
| 51 | | opts.positionals orelse |
| 52 | | arg_config.showHelpAndExit(program_name), |
| 272 | ctx.cwd, |
| 273 | opts.positionals orelse { |
| 274 | ctx.showHelp(@field(CommandId, @tagName(mode))); |
| 275 | Cli.fatal("missing entry file argument", .{}); |
| 276 | }, |
| 53 | 277 | }); |
| 54 | 278 | defer alloc.free(file_path); |
| 55 | 279 | |
| ... | ... | @@ -80,13 +304,19 @@ pub fn main() !u8 { |
| 80 | 304 | std.debug.print("Transform failed:\n", .{}); |
| 81 | 305 | try fail_report.dump(.initComp(&comp, main_file), comp.out); |
| 82 | 306 | fail_report.deinit(comp.gpa); |
| 83 | | return 1; |
| 307 | std.process.exit(1); |
| 84 | 308 | }, |
| 85 | 309 | error.ParseFail => { |
| 86 | 310 | // Already printed. |
| 87 | | return 1; |
| 311 | std.process.exit(1); |
| 312 | }, |
| 313 | else => |e| { |
| 314 | _ = @errorReturnTrace(); |
| 315 | Cli.fatal("failed to load {s}: {s}", .{ |
| 316 | file_path, |
| 317 | @errorName(e), |
| 318 | }); |
| 88 | 319 | }, |
| 89 | | else => return err, |
| 90 | 320 | }; |
| 91 | 321 | |
| 92 | 322 | if (comp.options.dump_ir) { |
| ... | ... | @@ -98,33 +328,56 @@ pub fn main() !u8 { |
| 98 | 328 | comp.runAllJobs() catch |err| switch (err) { |
| 99 | 329 | error.AnalysisFail => { |
| 100 | 330 | // error is already printed |
| 101 | | return 1; |
| 331 | std.process.exit(1); |
| 102 | 332 | }, |
| 103 | 333 | else => return err, |
| 104 | 334 | }; |
| 105 | 335 | |
| 106 | | if (config.have_embedded_quickjs and (opts.run orelse false)) { |
| 107 | | // Execute with QuickJS |
| 108 | | var out: std.ArrayListUnmanaged(u8) = .{}; |
| 109 | | defer out.deinit(alloc); |
| 110 | | try js_backend.link(&comp, entry_point, out.writer(alloc).any()); |
| 111 | | try out.append(alloc, 0); |
| 112 | | try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]); |
| 113 | | } else { |
| 114 | | // Output to standard out |
| 115 | | const output = std.io.getStdOut().writer(); |
| 116 | | var bufw = std.io.bufferedWriter(output); |
| 117 | | const writer = bufw.writer(); |
| 118 | | try js_backend.link(&comp, entry_point, writer.any()); |
| 119 | | try bufw.flush(); |
| 336 | switch (mode) { |
| 337 | .run => { |
| 338 | // Execute with QuickJS |
| 339 | var out: std.ArrayListUnmanaged(u8) = .{}; |
| 340 | defer out.deinit(alloc); |
| 341 | try js_backend.link(&comp, entry_point, out.writer(alloc).any()); |
| 342 | try out.append(alloc, 0); |
| 343 | try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]); |
| 344 | }, |
| 345 | .build => { |
| 346 | // Output to standard out |
| 347 | const output = std.io.getStdOut().writer(); |
| 348 | var bufw = std.io.bufferedWriter(output); |
| 349 | const writer = bufw.writer(); |
| 350 | try js_backend.link(&comp, entry_point, writer.any()); |
| 351 | try bufw.flush(); |
| 352 | }, |
| 120 | 353 | } |
| 121 | 354 | } |
| 122 | 355 | |
| 123 | 356 | std.process.cleanExit(); |
| 124 | | return 0; |
| 125 | 357 | } |
| 126 | 358 | |
| 359 | fn cmdBuild(ctx: Context) !void { |
| 360 | try cmdBuildOrRun(ctx, .build); |
| 361 | } |
| 362 | |
| 363 | fn cmdRun(ctx: Context) !void { |
| 364 | try cmdBuildOrRun(ctx, .run); |
| 365 | } |
| 366 | |
| 367 | // fn cmdFmt(ctx: Context) !void { |
| 368 | // const opts = ctx.options(.fmt); |
| 369 | // _ = opts; // autofix |
| 370 | // } |
| 371 | |
| 372 | const arg_config: Cli = .{ |
| 373 | .options = .{} ++ Cli.Option.optional(config.have_embedded_quickjs, &.{ |
| 374 | .{ .name = "--run", .desc = "Execute with QuickJS" }, |
| 375 | }), |
| 376 | .positionals = .single, |
| 377 | }; |
| 378 | |
| 127 | 379 | fn runWithQuickJs(js: [:0]const u8) !void { |
| 380 | comptime assert(config.have_embedded_quickjs); |
| 128 | 381 | const qjs = @import("quickjs"); |
| 129 | 382 | |
| 130 | 383 | const runtime = qjs.JS_NewRuntime() orelse |
| ... | ... | @@ -159,20 +412,24 @@ fn runWithQuickJs(js: [:0]const u8) !void { |
| 159 | 412 | |
| 160 | 413 | const slice = std.mem.span(exception_str); |
| 161 | 414 | |
| 162 | | std.debug.print("Exception: {s}\n", .{slice}); |
| 163 | | |
| 164 | | std.process.exit(1); |
| 415 | Cli.fatal("{s}", .{slice}); |
| 165 | 416 | } |
| 166 | 417 | } |
| 167 | 418 | |
| 419 | const config = @import("config"); |
| 420 | |
| 421 | const std = @import("std"); |
| 422 | const assert = std.debug.assert; |
| 423 | const Allocator = std.mem.Allocator; |
| 424 | |
| 168 | 425 | const core = @import("core"); |
| 426 | const OutStream = core.OutStream; |
| 169 | 427 | const Comp = core.Comp; |
| 170 | 428 | const Report = core.Report; |
| 171 | 429 | |
| 172 | 430 | const tracer = @import("tracer"); |
| 173 | 431 | pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); |
| 174 | 432 | |
| 175 | | const std = @import("std"); |
| 176 | 433 | const builtin = @import("builtin"); |
| 177 | 434 | const Cli = @import("Cli.zig"); |
| 178 | 435 | |