| ... | @@ -1,31 +1,155 @@ | ... | @@ -1,31 +1,155 @@ |
| 1 | //! This is the `lang` command line interface. It combines all components | 1 | //! This is the user-facing command line, combining all features into |
| 2 | //! of the software development kit into one. Multiple versions of this | 2 | //! different subcommands, each with their own option set. |
| 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. | | |
| 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 = .{ | 6 | const Command = struct { |
| 10 | .options = .{ | 7 | name: [:0]const u8, |
| 11 | .{ .short = 'i', .name = "--dump-ir", .desc = "Dump intermediate representations" }, | 8 | description: []const u8, |
| 12 | .{ .name = "--dump-state", .desc = "Dump compiler state to JSON", .kind = .string }, | 9 | cli: Cli, |
| 13 | } ++ Cli.Option.optional(config.have_embedded_quickjs, &.{ | 10 | run: fn (Context) anyerror!void, |
| 14 | .{ .name = "--run", .desc = "Execute with QuickJS" }, | 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 | try tracer.init(); | 140 | try tracer.init(); |
| 21 | defer tracer.deinit(); | 141 | defer tracer.deinit(); |
| 22 | try tracer.init_thread(null); | 142 | try tracer.init_thread(null); |
| 23 | defer tracer.deinit_thread(); | 143 | defer tracer.deinit_thread(); |
| 24 | | 144 | |
| 25 | // unicode props is asserted to be initialized before core.Tokenizer is used | 145 | // 'unicode_props' is asserted to be initialized when core.Tokenizer is used. |
| 26 | // this will result in a static page allocation, which is never cleaned up. | | |
| 27 | try core.unicode_props.init(); | 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 | const use_gpa = builtin.mode == .Debug; | 153 | const use_gpa = builtin.mode == .Debug; |
| 30 | var gpa = if (use_gpa) | 154 | var gpa = if (use_gpa) |
| 31 | std.heap.GeneralPurposeAllocator(.{ | 155 | std.heap.GeneralPurposeAllocator(.{ |
| ... | @@ -38,18 +162,118 @@ pub fn main() !u8 { | ... | @@ -38,18 +162,118 @@ pub fn main() !u8 { |
| 38 | | 162 | |
| 39 | // Current working directory is required for CLI resolution. | 163 | // Current working directory is required for CLI resolution. |
| 40 | var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; | 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 | // Command line argument parsing. | 170 | // Command line argument parsing. |
| 44 | const program_name = "zig build compiler --"; | 171 | const program_base_name, const args = try Cli.programArgsAlloc(alloc, default_program_name); |
| 45 | _, const args = try Cli.programArgsAlloc(alloc, program_name); | | |
| 46 | defer Cli.programArgsFree(alloc, args); | 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 | const file_path = try std.fs.path.resolve(alloc, &.{ | 271 | const file_path = try std.fs.path.resolve(alloc, &.{ |
| 50 | cwd, | 272 | ctx.cwd, |
| 51 | opts.positionals orelse | 273 | opts.positionals orelse { |
| 52 | arg_config.showHelpAndExit(program_name), | 274 | ctx.showHelp(@field(CommandId, @tagName(mode))); |
| | 275 | Cli.fatal("missing entry file argument", .{}); |
| | 276 | }, |
| 53 | }); | 277 | }); |
| 54 | defer alloc.free(file_path); | 278 | defer alloc.free(file_path); |
| 55 | | 279 | |
| ... | @@ -80,13 +304,19 @@ pub fn main() !u8 { | ... | @@ -80,13 +304,19 @@ pub fn main() !u8 { |
| 80 | std.debug.print("Transform failed:\n", .{}); | 304 | std.debug.print("Transform failed:\n", .{}); |
| 81 | try fail_report.dump(.initComp(&comp, main_file), comp.out); | 305 | try fail_report.dump(.initComp(&comp, main_file), comp.out); |
| 82 | fail_report.deinit(comp.gpa); | 306 | fail_report.deinit(comp.gpa); |
| 83 | return 1; | 307 | std.process.exit(1); |
| 84 | }, | 308 | }, |
| 85 | error.ParseFail => { | 309 | error.ParseFail => { |
| 86 | // Already printed. | 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 | if (comp.options.dump_ir) { | 322 | if (comp.options.dump_ir) { |
| ... | @@ -98,33 +328,56 @@ pub fn main() !u8 { | ... | @@ -98,33 +328,56 @@ pub fn main() !u8 { |
| 98 | comp.runAllJobs() catch |err| switch (err) { | 328 | comp.runAllJobs() catch |err| switch (err) { |
| 99 | error.AnalysisFail => { | 329 | error.AnalysisFail => { |
| 100 | // error is already printed | 330 | // error is already printed |
| 101 | return 1; | 331 | std.process.exit(1); |
| 102 | }, | 332 | }, |
| 103 | else => return err, | 333 | else => return err, |
| 104 | }; | 334 | }; |
| 105 | | 335 | |
| 106 | if (config.have_embedded_quickjs and (opts.run orelse false)) { | 336 | switch (mode) { |
| 107 | // Execute with QuickJS | 337 | .run => { |
| 108 | var out: std.ArrayListUnmanaged(u8) = .{}; | 338 | // Execute with QuickJS |
| 109 | defer out.deinit(alloc); | 339 | var out: std.ArrayListUnmanaged(u8) = .{}; |
| 110 | try js_backend.link(&comp, entry_point, out.writer(alloc).any()); | 340 | defer out.deinit(alloc); |
| 111 | try out.append(alloc, 0); | 341 | try js_backend.link(&comp, entry_point, out.writer(alloc).any()); |
| 112 | try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]); | 342 | try out.append(alloc, 0); |
| 113 | } else { | 343 | try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]); |
| 114 | // Output to standard out | 344 | }, |
| 115 | const output = std.io.getStdOut().writer(); | 345 | .build => { |
| 116 | var bufw = std.io.bufferedWriter(output); | 346 | // Output to standard out |
| 117 | const writer = bufw.writer(); | 347 | const output = std.io.getStdOut().writer(); |
| 118 | try js_backend.link(&comp, entry_point, writer.any()); | 348 | var bufw = std.io.bufferedWriter(output); |
| 119 | try bufw.flush(); | 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 | std.process.cleanExit(); | 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 | fn runWithQuickJs(js: [:0]const u8) !void { | 379 | fn runWithQuickJs(js: [:0]const u8) !void { |
| | 380 | comptime assert(config.have_embedded_quickjs); |
| 128 | const qjs = @import("quickjs"); | 381 | const qjs = @import("quickjs"); |
| 129 | | 382 | |
| 130 | const runtime = qjs.JS_NewRuntime() orelse | 383 | const runtime = qjs.JS_NewRuntime() orelse |
| ... | @@ -159,20 +412,24 @@ fn runWithQuickJs(js: [:0]const u8) !void { | ... | @@ -159,20 +412,24 @@ fn runWithQuickJs(js: [:0]const u8) !void { |
| 159 | | 412 | |
| 160 | const slice = std.mem.span(exception_str); | 413 | const slice = std.mem.span(exception_str); |
| 161 | | 414 | |
| 162 | std.debug.print("Exception: {s}\n", .{slice}); | 415 | Cli.fatal("{s}", .{slice}); |
| 163 | | | |
| 164 | std.process.exit(1); | | |
| 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 | const core = @import("core"); | 425 | const core = @import("core"); |
| | 426 | const OutStream = core.OutStream; |
| 169 | const Comp = core.Comp; | 427 | const Comp = core.Comp; |
| 170 | const Report = core.Report; | 428 | const Report = core.Report; |
| 171 | | 429 | |
| 172 | const tracer = @import("tracer"); | 430 | const tracer = @import("tracer"); |
| 173 | pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); | 431 | pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); |
| 174 | | 432 | |
| 175 | const std = @import("std"); | | |
| 176 | const builtin = @import("builtin"); | 433 | const builtin = @import("builtin"); |
| 177 | const Cli = @import("Cli.zig"); | 434 | const Cli = @import("Cli.zig"); |
| 178 | | 435 | |