| 1 | //! This is the user-facing command line, combining all features into |
| 2 | //! different subcommands, each with their own option set. |
| 3 | |
| 4 | const default_program_name = "ix"; // fallback when argv[0] is missing |
| 5 | |
| 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 ++ [_]Cli.Option{ |
| 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 |
| 80 | }), |
| 81 | }; |
| 82 | |
| 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 { |
| 140 | try tracer.init(); |
| 141 | defer tracer.deinit(); |
| 142 | try tracer.init_thread(null); |
| 143 | defer tracer.deinit_thread(); |
| 144 | |
| 145 | // 'unicode_props' is asserted to be initialized when core.Tokenizer is used. |
| 146 | try core.unicode_props.init(); |
| 147 | defer core.unicode_props.deinit(); |
| 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 |
| 153 | const use_gpa = builtin.mode == .Debug; |
| 154 | var gpa = if (use_gpa) |
| 155 | std.heap.GeneralPurposeAllocator(.{ |
| 156 | .stack_trace_frames = 100, |
| 157 | }){} |
| 158 | else |
| 159 | std.heap.ArenaAllocator.init(std.heap.page_allocator); |
| 160 | defer _ = if (use_gpa) gpa.deinit(); |
| 161 | const alloc = gpa.allocator(); |
| 162 | |
| 163 | // Current working directory is required for CLI resolution. |
| 164 | var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; |
| 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 | }; |
| 169 | |
| 170 | // Command line argument parsing. |
| 171 | const program_base_name, const args = try Cli.programArgsAlloc(alloc, default_program_name); |
| 172 | defer Cli.programArgsFree(alloc, 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; |
| 270 | |
| 271 | const file_path = try std.fs.path.resolve(alloc, &.{ |
| 272 | ctx.cwd, |
| 273 | opts.positionals orelse { |
| 274 | ctx.showHelp(@field(CommandId, @tagName(mode))); |
| 275 | Cli.fatal("missing entry file argument", .{}); |
| 276 | }, |
| 277 | }); |
| 278 | defer alloc.free(file_path); |
| 279 | |
| 280 | // Execute compiler |
| 281 | { |
| 282 | var js_backend = core.Js{}; |
| 283 | |
| 284 | var comp = try core.Comp.init(.{ |
| 285 | .gpa = tracer.tracingAllocator(&alloc), |
| 286 | .options = .{ |
| 287 | .dump_ir = opts.dump_ir orelse false, |
| 288 | }, |
| 289 | .backend = .{ .js = &js_backend }, |
| 290 | }); |
| 291 | defer comp.deinit(); |
| 292 | |
| 293 | defer if (opts.dump_state) |path| { |
| 294 | dumpState(&comp, path) catch |err| { |
| 295 | std.debug.print("Failed to dump compilation state: {}", .{err}); |
| 296 | }; |
| 297 | }; |
| 298 | |
| 299 | const main_file = try comp.getFile(file_path); |
| 300 | |
| 301 | var fail_report: Report = undefined; |
| 302 | const hir = main_file.getHir(&comp, &fail_report) catch |err| switch (err) { |
| 303 | error.TransformFail => { |
| 304 | std.debug.print("Transform failed:\n", .{}); |
| 305 | try fail_report.dump(.initComp(&comp, main_file), comp.out); |
| 306 | fail_report.deinit(comp.gpa); |
| 307 | std.process.exit(1); |
| 308 | }, |
| 309 | error.ParseFail => { |
| 310 | // Already printed. |
| 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 | }); |
| 319 | }, |
| 320 | }; |
| 321 | |
| 322 | if (comp.options.dump_ir) { |
| 323 | const stderr = std.io.getStdErr().writer(); |
| 324 | try hir.dumpToWriter(stderr, null); |
| 325 | } |
| 326 | |
| 327 | const entry_point = comp.enqueueEntryModule(main_file) catch |err| switch (err) { |
| 328 | error.TransformFail => std.process.exit(1), |
| 329 | else => |e| return e, |
| 330 | }; |
| 331 | comp.runAllJobs() catch |err| switch (err) { |
| 332 | error.AnalysisFail => { |
| 333 | // error is already printed |
| 334 | std.process.exit(1); |
| 335 | }, |
| 336 | else => return err, |
| 337 | }; |
| 338 | |
| 339 | switch (mode) { |
| 340 | .run => { |
| 341 | // Execute with QuickJS |
| 342 | var out: std.ArrayListUnmanaged(u8) = .{}; |
| 343 | defer out.deinit(alloc); |
| 344 | try js_backend.link(&comp, entry_point, out.writer(alloc).any()); |
| 345 | try out.append(alloc, 0); |
| 346 | try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]); |
| 347 | }, |
| 348 | .build => { |
| 349 | // Output to standard out |
| 350 | const output = std.io.getStdOut().writer(); |
| 351 | var bufw = std.io.bufferedWriter(output); |
| 352 | const writer = bufw.writer(); |
| 353 | try js_backend.link(&comp, entry_point, writer.any()); |
| 354 | try bufw.flush(); |
| 355 | }, |
| 356 | } |
| 357 | } |
| 358 | |
| 359 | std.process.cleanExit(); |
| 360 | } |
| 361 | |
| 362 | fn cmdBuild(ctx: Context) !void { |
| 363 | try cmdBuildOrRun(ctx, .build); |
| 364 | } |
| 365 | |
| 366 | fn cmdRun(ctx: Context) !void { |
| 367 | try cmdBuildOrRun(ctx, .run); |
| 368 | } |
| 369 | |
| 370 | // fn cmdFmt(ctx: Context) !void { |
| 371 | // const opts = ctx.options(.fmt); |
| 372 | // _ = opts; // autofix |
| 373 | // } |
| 374 | |
| 375 | const arg_config: Cli = .{ |
| 376 | .options = .{} ++ Cli.Option.optional(config.have_embedded_quickjs, &.{ |
| 377 | .{ .name = "--run", .desc = "Execute with QuickJS" }, |
| 378 | }), |
| 379 | .positionals = .single, |
| 380 | }; |
| 381 | |
| 382 | fn runWithQuickJs(js: [:0]const u8) !void { |
| 383 | comptime assert(config.have_embedded_quickjs); |
| 384 | const qjs = @import("quickjs"); |
| 385 | |
| 386 | const runtime = qjs.JS_NewRuntime() orelse |
| 387 | return error.UnexpectedError; |
| 388 | defer qjs.JS_FreeRuntime(runtime); |
| 389 | const js_ctx = qjs.JS_NewContext(runtime) orelse |
| 390 | return error.UnexpectedError; |
| 391 | defer qjs.JS_FreeContext(js_ctx); |
| 392 | |
| 393 | // QuickJS provides a completely blank global context. It is a non-goal to |
| 394 | // be a full runtime, but adding simple globals to align with expectations |
| 395 | // is OK. |
| 396 | |
| 397 | // 0 and null is zero process args |
| 398 | qjs.js_std_add_helpers(js_ctx, 0, null); |
| 399 | |
| 400 | const result = qjs.JS_Eval( |
| 401 | js_ctx, |
| 402 | js.ptr, |
| 403 | js.len, |
| 404 | "test.js", |
| 405 | qjs.JS_EVAL_TYPE_GLOBAL, |
| 406 | ); |
| 407 | defer qjs.JS_FreeValue(js_ctx, result); |
| 408 | |
| 409 | if (qjs.JS_IsException(result) != 0) { |
| 410 | const exception = qjs.JS_GetException(js_ctx); |
| 411 | defer qjs.JS_FreeValue(js_ctx, exception); |
| 412 | |
| 413 | const exception_str = qjs.JS_ToCString(js_ctx, exception); |
| 414 | defer qjs.JS_FreeCString(js_ctx, exception_str); |
| 415 | |
| 416 | const slice = std.mem.span(exception_str); |
| 417 | |
| 418 | Cli.fatal("{s}", .{slice}); |
| 419 | } |
| 420 | } |
| 421 | |
| 422 | const config = @import("config"); |
| 423 | |
| 424 | const std = @import("std"); |
| 425 | const assert = std.debug.assert; |
| 426 | const Allocator = std.mem.Allocator; |
| 427 | |
| 428 | const core = @import("core"); |
| 429 | const OutStream = core.OutStream; |
| 430 | const Comp = core.Comp; |
| 431 | const Report = core.Report; |
| 432 | |
| 433 | const tracer = @import("tracer"); |
| 434 | pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); |
| 435 | |
| 436 | const builtin = @import("builtin"); |
| 437 | const Cli = @import("Cli"); |
| 438 | |
| 439 | fn dumpState(comp: *const Comp, dump_path: []const u8) !void { |
| 440 | const file = try std.fs.cwd().createFile(dump_path, .{}); |
| 441 | defer file.close(); |
| 442 | |
| 443 | var bufw = std.io.bufferedWriter(file.writer()); |
| 444 | var jw = std.json.writeStream(bufw.writer(), .{ |
| 445 | .whitespace = .indent_2, |
| 446 | }); |
| 447 | |
| 448 | try comp.jsonStringify(&jw); |
| 449 | try bufw.writer().writeAll("\n"); |
| 450 | try bufw.flush(); |
| 451 | } |