1//! This is the user-facing command line, combining all features into
2//! different subcommands, each with their own option set.
3
4const default_program_name = "ix"; // fallback when argv[0] is missing
5
6const 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
23const 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
28const 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
83const 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
106const 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
127fn 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
135fn ParsedOptions(subcommand: CommandId) type {
136 return getSubCommand(subcommand).cli.Parsed();
137}
138
139pub 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
237fn 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
267fn 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
362fn cmdBuild(ctx: Context) !void {
363 try cmdBuildOrRun(ctx, .build);
364}
365
366fn 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
375const 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
382fn 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
422const config = @import("config");
423
424const std = @import("std");
425const assert = std.debug.assert;
426const Allocator = std.mem.Allocator;
427
428const core = @import("core");
429const OutStream = core.OutStream;
430const Comp = core.Comp;
431const Report = core.Report;
432
433const tracer = @import("tracer");
434pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));
435
436const builtin = @import("builtin");
437const Cli = @import("Cli");
438
439fn 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}