authorchloe caruso <git@paperclover.net> 2024-09-14 01:47:20-04:00
committerchloe caruso <git@paperclover.net> 2024-09-14 01:47:20-04:00
log26118bf1f3068249f8bf76edccaf663c0c5518c8
tree2b3bea1fb746eb4613cc4fdd5e76072f5986feb0
parentcd425afcbd7d3d51c85e8d68cdfd2bf29e7c1b1d

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'

6 files changed, 1032 insertions(+), 352 deletions(-)

build.zig+34-6
......@@ -25,6 +25,7 @@ const ArtifactGroup = struct {
2525 opts: Options,
2626
2727 cli: *Step.Compile,
28 compiler_cli: *Step.Compile,
2829 format_cli: *Step.Compile,
2930 transform_cli: *Step.Compile,
3031 loader_cli: *Step.Compile,
......@@ -58,8 +59,16 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
5859 break :config_mod config.createModule();
5960 };
6061
62 // Cli helper
63 const cli_mod = b.createModule(.{
64 .root_source_file = b.path("src/Cli.zig"),
65 .optimize = options.optimize,
66 .target = options.target,
67 .strip = false,
68 });
69
6170 // Core
62 const core_mod = b.addModule("core", .{
71 const core_mod = b.createModule(.{
6372 .root_source_file = b.path("src/core/mod.zig"),
6473 .optimize = options.optimize,
6574 .target = options.target,
......@@ -70,7 +79,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
7079 core_mod.addImport("zg:PropsData", zg.module("PropsData"));
7180
7281 // Format
73 const format_mod = b.addModule("format", .{
82 const format_mod = b.createModule(.{
7483 .root_source_file = b.path("src/format/Format.zig"),
7584 .optimize = options.optimize,
7685 .target = options.target,
......@@ -81,7 +90,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
8190 format_mod.addImport("tracer", tracer.module("tracer"));
8291
8392 // Loader
84 const loader_mod = b.addModule("loader", .{
93 const loader_mod = b.createModule(.{
8594 .root_source_file = b.path("src/loader/Loader.zig"),
8695 .optimize = options.optimize,
8796 .target = options.target,
......@@ -107,11 +116,12 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
107116 compiler_tests.root_module.addImport("config", config_mod);
108117 compiler_tests.root_module.addImport("tracer", tracer.module("tracer"));
109118 compiler_tests.root_module.addImport("quickjs", quickjs.module("quickjs"));
119 compiler_tests.root_module.addImport("Cli", cli_mod);
110120
111121 // Main CLI
112122 const cli = b.addExecutable(.{
113123 .name = "compiler",
114 .root_source_file = b.path("src/cli.zig"),
124 .root_source_file = b.path("src/main.zig"),
115125 .target = options.target,
116126 .optimize = options.optimize,
117127 .use_lld = use_lld,
......@@ -125,6 +135,23 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
125135 cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
126136 }
127137
138 // Compiler Dev CLI
139 const compiler_cli = b.addExecutable(.{
140 .name = "compiler",
141 .root_source_file = b.path("src/cli_compiler.zig"),
142 .target = options.target,
143 .optimize = options.optimize,
144 .use_lld = use_lld,
145 .use_llvm = use_llvm,
146 .single_threaded = true,
147 });
148 compiler_cli.root_module.addImport("core", core_mod);
149 compiler_cli.root_module.addImport("config", config_mod);
150 compiler_cli.root_module.addImport("tracer", tracer.module("tracer"));
151 if (options.config.have_embedded_quickjs) {
152 cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc"));
153 }
154
128155 const format_cli = b.addExecutable(.{
129156 .name = "format",
130157 .root_source_file = b.path("src/format/format_cli.zig"),
......@@ -154,7 +181,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
154181
155182 const transform_cli = b.addExecutable(.{
156183 .name = "transform",
157 .root_source_file = b.path("src/core/transform_cli.zig"),
184 .root_source_file = b.path("src/cli_transform.zig"),
158185 .target = options.target,
159186 .optimize = options.optimize,
160187 .use_lld = use_lld,
......@@ -170,6 +197,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup {
170197 .opts = options,
171198
172199 .cli = cli,
200 .compiler_cli = compiler_cli,
173201 .format_cli = format_cli,
174202 .loader_cli = loader_cli,
175203 .transform_cli = transform_cli,
......@@ -372,7 +400,7 @@ pub fn build(b: *Build) !void {
372400 b.installArtifact(main.cli);
373401
374402 inline for (.{
375 .{ minimal.cli, "compiler" },
403 .{ minimal.compiler_cli, "compiler" },
376404 .{ minimal.format_cli, "format" },
377405 .{ minimal.loader_cli, "loader" },
378406 .{ minimal.transform_cli, "transform" },
src/cli.zig+613-217
......@@ -1,260 +1,656 @@
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.
6
7const have_debug_features = builtin.mode == .Debug;
8const config = @import("config");
9
10pub fn main() !u8 {
11 try tracer.init();
12 defer tracer.deinit();
13 try tracer.init_thread(null);
14 defer tracer.deinit_thread();
15
16 // unicode props is asserted to be initialized before core.Tokenizer is used
17 // this will result in a static page allocation, which is never cleaned up.
18 try core.unicode_props.init();
19
20 const use_gpa = builtin.mode == .Debug;
21 var gpa = if (use_gpa)
22 std.heap.GeneralPurposeAllocator(.{
23 .stack_trace_frames = 100,
24 }){}
25 else
26 std.heap.ArenaAllocator.init(std.heap.page_allocator);
27 var no_leak_checks = false;
28 defer _ = if (use_gpa and !no_leak_checks) gpa.deinit();
29 const alloc = gpa.allocator();
1//! Configuration for an command line interface, which can be executed against
2//! an argument slice using `.parse`. None of the parsing strategies require an
3//! allocator, however it expects incoming arguments to be UTF-8 formatted which
4//! on Windows will require an allocator.
5//!
6//! See `fn main` for a basic usage example.
7options: []const Option = &.{},
8positionals: Positionals = .none,
9help: HelpConfig = .{},
10
11const Option = struct {
12 /// Should be formatted as the flag name: '--dump-ir'
13 name: []const u8,
14 /// Displayed in generated help menu
15 desc: []const u8,
16 kind: Kind = .boolean,
17 /// A short code allows single dash argument parsing.
18 short: ?u8 = null,
19
20 const Kind = union(enum) {
21 /// UTF-8 validated
22 string,
23 boolean,
24 @"enum": type,
25
26 pub fn Type(k: Kind) type {
27 return switch (k) {
28 .string => []const u8,
29 .boolean => bool,
30 .@"enum" => |t| t,
31 };
32 }
33 };
34
35 const help: Option = .{
36 .name = "--help",
37 .short = 'h',
38 .desc = "Print this help and exit",
39 };
40};
41
42pub const Positionals = union(enum) {
43 none,
44 /// Store a single positional: ?[]const u8
45 single,
46 /// Store the start and count of positionals. Call
47 /// 'parsed.positionals.iterator(args)' with the
48 /// original argument array to read positionals.
49 unlimited,
50 /// Same strategy as .unlimited, with a limit
51 limited: usize,
52 /// Use a bounded array to store positionals, using
53 /// a stack allocated buffer that can hold enough
54 /// '[]const u8's as allowed by the bound.
55 bounded: usize,
56
57 pub fn Type(p: Positionals) type {
58 return switch (p) {
59 .none => void,
60 .single => ?[]const u8,
61 .limited => |limit| switch (limit) {
62 0 => void,
63 else => Multi,
64 },
65 .unlimited => Multi,
66 .bounded => |n| std.BoundedArray([]const u8, n),
67 };
68 }
3069
31 // Current working directory is required for CLI resolution.
32 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
33 const cwd = try std.posix.getcwd(&cwd_buf);
34
35 // Command line argument parsing.
36 var maybe_file_path: ?[]const u8 = null;
37 defer if (maybe_file_path) |path| alloc.free(path);
38 var exe_name: ?[]const u8 = null;
39 defer if (exe_name) |path| alloc.free(path);
40 var dump_ir = false;
41 var dump_state: ?[]const u8 = null;
42 var run = false;
43 var has_errors = false;
44 {
45 var args = try std.process.argsWithAllocator(alloc);
46 defer args.deinit();
47
48 exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli"));
49
50 while (args.next()) |arg| {
51 if (std.mem.startsWith(u8, arg, "-")) {
52 if (have_debug_features) {
53 if (std.mem.eql(u8, arg, "--report-trace")) {
54 core.Report.debug_show_report_trace = true;
55 continue;
56 }
57 if (std.mem.eql(u8, arg, "--no-leak-checks")) {
58 no_leak_checks = true;
59 continue;
60 }
61 }
70 pub inline fn maxCount(p: Positionals) usize {
71 return switch (p) {
72 .none => 0,
73 .single => 1,
74 inline .limited, .bounded => |limit| limit,
75 .unlimited => std.math.maxInt(usize),
76 };
77 }
78
79 pub const Multi = struct {
80 /// The index of the first positional
81 first: usize,
82 /// The number of total positionals. This can
83 /// be used to pre-allocate resources.
84 len: usize,
85
86 first_is_after_double_dash: bool,
6287
63 if (std.mem.eql(u8, arg, "--dump-ir")) {
64 dump_ir = true;
65 continue;
88 pub const empty = .{
89 .first = 0,
90 .len = 0,
91 .first_is_after_double_dash = false,
92 };
93
94 pub fn iterator(pos: @This(), all_args: []const []const u8) PositionalIterator {
95 return .{
96 .args = all_args,
97 .i = pos.first,
98 .left = pos.len,
99 .after_double_dash = pos.first_is_after_double_dash,
100 };
101 }
102 };
103};
104
105pub const HelpConfig = struct {
106 /// If set true, registers an addition '--help' / '-h' flag
107 /// These will show the help menu and exit the program.
108 ///
109 /// Help menu can always be displayed manually with .showHelp()
110 option: bool = true,
111
112 /// Override the usage string in the first line.
113 usage: ?[]const u8 = null,
114
115 /// Hide a list of flag names.
116 hide_options: []const []const u8 = &.{},
117};
118
119pub const PositionalIterator = struct {
120 args: []const []const u8,
121 i: usize,
122 left: usize,
123 after_double_dash: bool,
124
125 pub fn peek(it: PositionalIterator) ?[]const u8 {
126 return if (it.left > 0) it.args[it.i] else null;
127 }
128
129 pub fn next(it: *PositionalIterator) ?[]const u8 {
130 if (it.left > 0) {
131 defer it.advance();
132 return it.args[it.i];
133 } else {
134 return null;
135 }
136 }
137
138 pub fn advance(it: *PositionalIterator) void {
139 it.left -= 1;
140 if (it.left == 0) return;
141 if (it.after_double_dash) {
142 it.i += 1;
143 } else while (mem.startsWith(u8, it.args[it.i], "-")) {
144 defer it.i += 1;
145 if (it.args[it.i].len == 2 and it.args[it.i][1] == '-') {
146 it.after_double_dash = true;
147 return;
148 }
149 }
150 }
151};
152
153inline fn fieldName(comptime name: []const u8) [:0]const u8 {
154 std.debug.assert(name[0] == '-' and name[1] == '-');
155 for (name[2..]) |char| switch (char) {
156 'a'...'z', '-' => {},
157 else => |ch| @compileError(std.fmt.comptimePrint("Unexpected char '{c}' in option name '{s}'", .{ ch, name })),
158 };
159
160 const replacement_size = mem.replacementSize(u8, name[2..], "-", "_");
161 const replaced = brk: {
162 var buf: [replacement_size:0]u8 = undefined;
163 _ = mem.replace(u8, name[2..], "-", "_", &buf);
164 break :brk buf;
165 };
166 return &replaced;
167}
168
169/// Every option name is converted into snake case, then
170/// an additional field name 'positionals' is added.
171pub fn Parsed(p: Cli) type {
172 const fields = brk: {
173 var fields: [p.options.len + 1]Type.StructField = undefined;
174
175 for (p.options, fields[0..p.options.len]) |def, *field| {
176 const Wrapped = ?def.kind.Type();
177 field.* = .{
178 .name = fieldName(def.name),
179 .alignment = @alignOf(Wrapped),
180 .default_value = &@as(Wrapped, null),
181 .is_comptime = false,
182 .type = Wrapped,
183 };
184 }
185
186 const Pos = p.positionals.Type();
187 fields[fields.len - 1] = .{
188 .name = "positionals",
189 .alignment = @alignOf(Pos),
190 .type = Pos,
191 .default_value = &@as(Pos, switch (p.positionals) {
192 .none => {},
193 .single => null,
194 .limited, .unlimited => .empty,
195 .bounded => .{},
196 }),
197 .is_comptime = false,
198 };
199
200 break :brk fields;
201 };
202
203 return @Type(.{ .@"struct" = .{
204 .layout = .auto,
205 .fields = &fields,
206 .decls = &.{},
207 .is_tuple = false,
208 } });
209}
210
211const StdErrWriter = struct {
212 file: std.fs.File,
213 buffered_writer: std.io.BufferedWriter(4096, std.fs.File.Writer),
214 tty: std.io.tty.Config,
215 writer_ptr: std.io.BufferedWriter(4096, std.fs.File.Writer).Writer,
216
217 pub fn init() StdErrWriter {
218 const stderr = std.io.getStdErr();
219 const tty = std.io.tty.detectConfig(stderr);
220 return .{
221 .file = stderr,
222 .tty = tty,
223 .buffered_writer = std.io.bufferedWriter(stderr.writer()),
224 .writer_ptr = undefined,
225 };
226 }
227
228 pub fn get(w: *StdErrWriter) std.io.AnyWriter {
229 if (@import("builtin").os.tag == .windows and w.tty == .windows_api) {
230 // all data must be flushed before changing colors using
231 // the Windows API. thankfully, modern windows terminals
232 // will not hit this path.
233 return w.buffered_writer.unbuffered_writer.any();
234 } else {
235 w.writer_ptr = w.buffered_writer.writer(); // uaf unless stored
236 return w.writer_ptr.any();
237 }
238 }
239};
240
241const ParseError = error{ InvalidArguments, DisplayedHelpMenu };
242
243/// This will quit the program with usage information if invalid arguments are
244/// received and the `--help` option is enabled. This is the most common use
245/// case. If manual error handling is desired, `.parseWithErrorWriter` can
246/// be used instead.
247pub fn parse(
248 comptime p: Cli,
249 program_name: []const u8,
250 args: []const []const u8,
251) p.Parsed() {
252 var stderr = StdErrWriter.init();
253 defer stderr.buffered_writer.flush() catch {};
254 return parseWithErrorWriter(
255 p,
256 program_name,
257 args,
258 stderr.get(),
259 stderr.tty,
260 true,
261 true,
262 ) catch |e| switch (e) {
263 error.InvalidArguments,
264 error.DisplayedHelpMenu,
265 => {
266 stderr.buffered_writer.flush() catch {};
267 std.process.exit(1);
268 },
269 };
270}
271
272pub fn parseWithErrorWriter(
273 comptime p: Cli,
274 program_name: []const u8,
275 args: []const []const u8,
276 err: std.io.AnyWriter,
277 tty: std.io.tty.Config,
278 comptime show_help_on_error: bool,
279 comptime exit_on_error: bool,
280) ParseError!p.Parsed() {
281 var out: p.Parsed() = .{};
282 var had_errors = false;
283 var positionals_counted: usize = 0;
284 var first_positional: usize = 0;
285 var double_dash: ?usize = null;
286
287 var show_help = false;
288
289 const max_positionals = p.positionals.maxCount();
290
291 outer: for (args, 0..) |arg, i| {
292 if (mem.startsWith(u8, arg, "--")) {
293 if (arg.len == 2) {
294 if (max_positionals == 0) {
295 writeError(err, tty);
296 err.writeAll("unexpected end-of-parameter mark: --\n") catch {};
66297 }
67298
68 if (std.mem.startsWith(u8, arg, "--dump-state=")) {
69 dump_state = arg["--dump-state=".len..];
70 continue;
299 double_dash = i;
300 const remain = args[i + 1 ..];
301 if (positionals_counted == 0 and remain.len > 0)
302 first_positional = i + 1;
303
304 if (p.positionals == .bounded) {
305 out.positionals.appendSliceAssumeCapacity(remain[0..@min(remain.len, max_positionals -| positionals_counted)]);
71306 }
72307
73 if (config.have_embedded_quickjs and std.mem.eql(u8, arg, "--run")) {
74 run = true;
75 continue;
308 positionals_counted += remain.len;
309
310 if (positionals_counted > max_positionals) {
311 had_errors = true;
312 const diff = @min(remain.len, positionals_counted - max_positionals);
313 for (remain[remain.len - diff ..]) |pos| {
314 writeError(err, tty);
315 err.print("unrecognized argument '{s}'\n", .{pos}) catch {};
316 }
76317 }
77318
78 std.debug.print("error: unknown flag '{s}'\n", .{arg});
79 has_errors = true;
80319 break;
81320 }
82321
83 if (maybe_file_path == null) {
84 maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg });
85 } else {
86 std.debug.print("error: multiple files specified\n", .{});
87 maybe_file_path = null;
88 break;
322 inline for (p.options) |def| try_this_def: {
323 const prefix = def.name;
324 const kind = def.kind;
325 const dest = &@field(out, fieldName(def.name));
326
327 const is_blank = arg.len == prefix.len;
328 if (!(mem.startsWith(u8, arg[2..], prefix[2..]) and (is_blank or arg[prefix.len] == '=')))
329 break :try_this_def;
330
331 if (kind != .boolean and is_blank) {
332 had_errors = true;
333 writeError(err, tty);
334 err.print(prefix ++ " requires a value\n", .{}) catch {};
335 continue :outer;
336 }
337
338 const value = if (is_blank) "" else arg[prefix.len + 1 ..];
339 switch (kind) {
340 .boolean => if (value.len == 0) {
341 dest.* = true;
342 } else if (mem.eql(u8, value, "false")) {
343 dest.* = false;
344 } else if (mem.eql(u8, value, "true")) {
345 dest.* = true;
346 } else {
347 had_errors = true;
348 writeError(err, tty);
349 err.print(prefix ++ " only accepts true or false, got '{s}'\n", .{
350 arg[prefix.len + 1 ..],
351 }) catch {};
352 },
353
354 .string => if (std.unicode.utf8ValidateSlice(value)) {
355 dest.* = value;
356 } else {
357 had_errors = true;
358 writeError(err, tty);
359 err.print(prefix ++ " requires valid UTF-8 input\n", .{}) catch {};
360 },
361
362 .@"enum" => |E| {
363 dest.* = std.meta.stringToEnum(E, value) orelse {
364 had_errors = true;
365 const values_list = comptime brk: {
366 var str: []const u8 = "";
367 for (std.enums.values(E)) |val| {
368 str = str ++ " " ++ @tagName(val) ++ "\n";
369 }
370 break :brk str;
371 };
372 writeError(err, tty);
373 err.print(prefix ++ " must be one of the following values, got {s}:\n{s}", .{
374 arg[prefix.len + 1 ..],
375 values_list,
376 }) catch {};
377 continue :outer;
378 };
379 },
380 }
381
382 continue :outer;
383 }
384
385 if (p.help.option) {
386 if (mem.startsWith(u8, arg[2..], "help") and (arg.len == "--help".len or arg["--help".len] == '=')) {
387 show_help = true;
388 if (arg.len > "--help".len) {
389 had_errors = true;
390 err.print("--help does not take a value\n", .{}) catch {};
391 }
392 continue :outer;
393 }
394 }
395
396 had_errors = true;
397 writeError(err, tty);
398 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
399 } else if (arg.len > 1 and arg[0] == '-') {
400 per_char_in_short: for (arg[1..]) |ch| {
401 inline for (p.options) |def| try_this_def: {
402 const short = def.short orelse break :try_this_def;
403 if (def.kind != .boolean) {
404 @compileError("TODO: non-boolean shorthand arguments");
405 }
406 if (ch == short) {
407 const dest = &@field(out, fieldName(def.name));
408 dest.* = true;
409 continue :per_char_in_short;
410 }
411 if (p.help.option and ch == 'h') {
412 show_help = true;
413 }
414 }
415 }
416 } else {
417 if (positionals_counted == 0) first_positional = i;
418 positionals_counted += 1;
419 if (positionals_counted > max_positionals) {
420 had_errors = true;
421 writeError(err, tty);
422 err.print("unrecognized argument '{s}'\n", .{arg}) catch {};
423 } else if (p.positionals == .bounded) {
424 out.positionals.appendAssumeCapacity(arg);
89425 }
90426 }
91427 }
92428
93 if (maybe_file_path == null) has_errors = true;
94 if (has_errors) {
95 std.debug.print("Usage: {s} [flags] <input file>\n", .{exe_name.?});
96 {
97 std.debug.print(
98 // | 60 chars wrap point|
99 \\
100 \\Flags:
101 \\ --dump-ir Dump intermediate representations
102 \\ --dump-state=<path> Dump compiler state to JSON
103 \\
104 , .{});
429 if (had_errors) {
430 if (positionals_counted > max_positionals) {
431 writeNote(err, tty);
432 err.writeAll(switch (max_positionals) {
433 0 => "this program does not take positional arguments\n",
434 1 => "this program accepts one positional argument\n",
435 else => |n| std.fmt.comptimePrint(
436 "this program accepts up to {d} positional arguments\n",
437 .{n},
438 ),
439 }) catch {};
105440 }
106 if (config.have_embedded_quickjs) {
107 std.debug.print(
108 \\ --run Run program using QuickJS
109 \\
110 , .{});
441 if (show_help or show_help_on_error) {
442 p.showHelpWithWriter(program_name, err, tty);
111443 }
112 if (have_debug_features) {
113 std.debug.print(
114 \\
115 \\Debug Mode Flags:
116 \\ --report-trace Print stack traces per error
117 \\ --no-leak-checks
118 \\
119 , .{});
444 if (exit_on_error) {
445 return error.DisplayedHelpMenu;
120446 }
121 std.debug.print("\nLanguage SDK v" ++ config.version_string ++ "\n", .{});
122 return 1;
447 return error.InvalidArguments;
123448 }
124 const file_path = maybe_file_path.?;
125449
126 // Execute compiler
127 {
128 var js_backend = core.Js{};
129
130 var comp = try core.Comp.init(.{
131 .gpa = tracer.tracingAllocator(&alloc),
132 .options = .{
133 .dump_ir = dump_ir,
134 },
135 .backend = .{ .js = &js_backend },
136 });
137 defer comp.deinit();
450 if (show_help) {
451 p.showHelpWithWriter(program_name, err, tty);
452 return error.DisplayedHelpMenu;
453 }
138454
139 defer if (dump_state) |path| {
140 dumpState(&comp, path) catch |err| {
141 std.debug.print("Failed to dump compilation state: {}", .{err});
142 };
143 };
455 out.positionals = switch (p.positionals) {
456 .none => {},
457 .single => if (positionals_counted > 0)
458 args[first_positional]
459 else
460 null,
461 .bounded => out.positionals, // already correct
462 .unlimited, .limited => if (max_positionals > 0) .{
463 .first = first_positional,
464 .len = positionals_counted,
465 .first_is_after_double_dash = if (double_dash) |dd|
466 dd < first_positional
467 else
468 false,
469 },
470 };
471
472 return out;
473}
144474
145 const main_file = try comp.getFile(file_path);
475pub fn showHelp(comptime p: Cli, program_name: []const u8) void {
476 var stderr = StdErrWriter.init();
477 defer stderr.buffered_writer.flush() catch {};
478 return showHelpWithWriter(
479 p,
480 program_name,
481 stderr.get(),
482 stderr.tty,
483 );
484}
146485
147 var fail_report: Report = undefined;
148 const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) {
149 error.TransformFail => {
150 std.debug.print("Transform failed:\n", .{});
151 try fail_report.dump(.initComp(&comp, main_file), comp.out);
152 fail_report.deinit(comp.gpa);
153 return 1;
154 },
155 error.ParseFail => {
156 // Already printed.
157 return 1;
158 },
159 else => return err,
486pub fn showHelpWithWriter(
487 comptime p: Cli,
488 program_name: []const u8,
489 w: std.io.AnyWriter,
490 tty: std.io.tty.Config,
491) void {
492 const usage = p.help.usage orelse default: {
493 const opts_usage = if (p.options.len > 0 or p.help.option) "[options...]" else "";
494 break :default switch (p.positionals.maxCount()) {
495 0 => opts_usage,
496 1 => mem.trimLeft(u8, opts_usage ++ " [argument]", " "),
497 else => mem.trimLeft(u8, opts_usage ++ " [args...]", " "),
160498 };
499 };
500
501 w.print("usage: {s} {s}\n", .{ program_name, usage }) catch return;
502
503 if (p.options.len > 0 or p.help.option) {
504 w.writeAll("\n") catch return;
505 tty.setColor(w, .bold) catch return;
506 w.writeAll("options:") catch return;
507 tty.setColor(w, .reset) catch return;
508 w.writeAll(comptime brk: {
509 var width = 0;
510 var any_short_options = false;
511
512 const all_opts = if (p.help.option)
513 p.options ++ [_]Option{.help}
514 else
515 p.options;
516 for (all_opts) |opt| {
517 width = @max(width, opt.name.len);
518 any_short_options = any_short_options or opt.short != null;
519 }
161520
162 if (comp.options.dump_ir) {
163 const stderr = std.io.getStdErr().writer();
164 try teg.dumpToWriter(stderr, null);
165 }
166
167 const entry_point = try comp.enqueueEntryModule(main_file);
168 comp.runAllJobs() catch |err| switch (err) {
169 error.AnalysisFail => {
170 // error is already printed
171 return 1;
172 },
173 else => return err,
174 };
521 width += " ".len;
522 if (any_short_options) width += "-h ".len;
523 const sub = if (any_short_options) 4 else 1;
175524
176 if (config.have_embedded_quickjs and run) {
177 // Execute with QuickJS
178 var out: std.ArrayListUnmanaged(u8) = .{};
179 defer out.deinit(alloc);
180 try js_backend.link(&comp, entry_point, out.writer(alloc).any());
181 try out.append(alloc, 0);
182 try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]);
183 } else {
184 // Output to standard out
185 const output = std.io.getStdOut().writer();
186 var bufw = std.io.bufferedWriter(output);
187 const writer = bufw.writer();
188 try js_backend.link(&comp, entry_point, writer.any());
189 try bufw.flush();
190 }
525 var out: []const u8 = "\n";
526 for (all_opts) |opt| {
527 out = out ++ " ";
528 if (any_short_options) {
529 out = out ++ if (opt.short) |s| [_]u8{ '-', s, ' ' } else " ";
530 }
531 out = out ++ opt.name ++ (" " ** (width - opt.name.len - sub));
532 // TODO: wrapping at 72 columns
533 out = out ++ opt.desc ++ "\n";
534 }
535 break :brk out ++ "\n";
536 }) catch return;
191537 }
192
193 std.process.cleanExit();
194 return 0;
195538}
196539
197fn runWithQuickJs(js: [:0]const u8) !void {
198 const qjs = @import("quickjs");
199
200 const runtime = qjs.JS_NewRuntime() orelse
201 return error.UnexpectedError;
202 defer qjs.JS_FreeRuntime(runtime);
203 const js_ctx = qjs.JS_NewContext(runtime) orelse
204 return error.UnexpectedError;
205 defer qjs.JS_FreeContext(js_ctx);
206
207 // QuickJS provides a completely blank global context. It is a non-goal to
208 // be a full runtime, but adding simple globals to align with expectations
209 // is OK.
210
211 // 0 and null is zero process args
212 qjs.js_std_add_helpers(js_ctx, 0, null);
213
214 const result = qjs.JS_Eval(
215 js_ctx,
216 js.ptr,
217 js.len,
218 "test.js",
219 qjs.JS_EVAL_TYPE_GLOBAL,
220 );
221 defer qjs.JS_FreeValue(js_ctx, result);
540pub fn showHelpAndExit(comptime p: Cli, program_name: []const u8) noreturn {
541 p.showHelp(program_name);
542 std.process.exit(1);
543}
222544
223 if (qjs.JS_IsException(result) != 0) {
224 const exception = qjs.JS_GetException(js_ctx);
225 defer qjs.JS_FreeValue(js_ctx, exception);
545fn writeError(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
546 tty.setColor(err, .red) catch return;
547 tty.setColor(err, .bold) catch return;
548 err.writeAll("error") catch return;
549 tty.setColor(err, .reset) catch return;
550 tty.setColor(err, .dim) catch return;
551 err.writeAll(": ") catch return;
552 tty.setColor(err, .reset) catch return;
553}
226554
227 const exception_str = qjs.JS_ToCString(js_ctx, exception);
228 defer qjs.JS_FreeCString(js_ctx, exception_str);
555fn writeNote(err: std.io.AnyWriter, tty: std.io.tty.Config) void {
556 tty.setColor(err, .blue) catch return;
557 tty.setColor(err, .bold) catch return;
558 err.writeAll("note") catch return;
559 tty.setColor(err, .reset) catch return;
560 tty.setColor(err, .dim) catch return;
561 err.writeAll(": ") catch return;
562 tty.setColor(err, .reset) catch return;
563}
229564
230 const slice = std.mem.span(exception_str);
565/// Splits args[0] containing the program name
566///
567/// const program_name, const args = programArgsAlloc();
568pub fn programArgsAlloc(allocator: std.mem.Allocator, fallback_name: []const u8) !struct { []const u8, [][:0]u8 } {
569 const magic_number = @alignOf([*][:0]u8);
570 var raw_args = try std.process.argsAlloc(allocator);
571 switch (raw_args.len) {
572 0 => {
573 raw_args.ptr = @ptrFromInt(magic_number);
574 return .{ fallback_name, raw_args };
575 },
576 else => return .{
577 std.fs.path.basename(raw_args[0]),
578 raw_args[1..],
579 },
580 }
581}
231582
232 std.debug.print("Exception: {s}\n", .{slice});
583pub fn programArgsFree(allocator: std.mem.Allocator, args: [][:0]u8) void {
584 const magic_number = @alignOf([*][:0]u8);
585 if (args.len == 0 and @intFromPtr(args.ptr) == magic_number) return;
586 const raw_args = (args.ptr - 1)[0 .. args.len + 1];
587 std.process.argsFree(allocator, raw_args);
588}
233589
234 std.process.exit(1);
590pub fn fatalWithHelp(p: Cli, program_name: []const u8, comptime fmt: []const u8, args: anytype) noreturn {
591 print: {
592 const stderr = std.io.getStdErr();
593 const tty = std.io.tty.detectConfig(stderr);
594 var bufw = std.io.bufferedWriter(stderr.writer());
595 writeError(bufw.writer().any(), tty);
596 bufw.writer().print(fmt ++ "\n", args) catch break :print;
597 p.showHelpWithWriter(program_name, bufw.writer().any(), tty);
598 bufw.flush() catch break :print;
235599 }
600 std.process.exit(1);
236601}
237602
238const core = @import("core");
239const Comp = core.Comp;
240const Report = core.Report;
603pub fn fatal(comptime fmt: []const u8, args: anytype) noreturn {
604 const stderr = std.io.getStdErr();
605 const tty = std.io.tty.detectConfig(stderr);
606 var bufw = std.io.bufferedWriter(stderr.writer());
607 writeError(bufw.writer().any(), tty);
608 bufw.writer().print(fmt ++ "\n", args);
609 bufw.flush() catch {}; // ignoring I/O errors
610 std.process.exit(1);
611}
241612
242const tracer = @import("tracer");
243pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));
613pub fn main() !void {
614 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
615 defer _ = gpa.deinit();
616 const alloc = gpa.allocator();
244617
245const std = @import("std");
246const builtin = @import("builtin");
618 const config: Cli = .{
619 .options = &.{
620 .{ .name = "--flag", .desc = "check for something" },
621 .{ .name = "--str", .desc = "pass a string", .kind = .string },
622 .{
623 .name = "--enum",
624 .desc = "enum",
625 .kind = .{ .@"enum" = std.builtin.OptimizeMode },
626 },
627 },
628 .positionals = .{ .bounded = 2 },
629 };
630
631 const program_name, const args = try Cli.programArgsAlloc(alloc);
632 defer Cli.programArgsFree(alloc, args);
633
634 const parsed = config.parse(args[1..]) catch
635 config.showHelpAndExit(program_name);
247636
248fn dumpState(comp: *const Comp, dump_path: []const u8) !void {
249 const file = try std.fs.cwd().createFile(dump_path, .{});
250 defer file.close();
637 std.debug.print("--flag: {any}\n", .{parsed.flag});
638 std.debug.print("--str: {?s}\n", .{parsed.str});
639 std.debug.print("--enum: {any}\n", .{parsed.@"enum"});
640 std.debug.print("positionals({d}):\n", .{parsed.positionals.len});
251641
252 var bufw = std.io.bufferedWriter(file.writer());
253 var jw = std.json.writeStream(bufw.writer(), .{
254 .whitespace = .indent_2,
255 });
642 // if using '.positionals = .unlimited'
643 // var it = parsed.positionals.iterator(args[1..]);
644 // while (it.next()) |item| {
645 // std.debug.print(" - {s}\n", .{item});
646 // }
256647
257 try comp.jsonStringify(&jw);
258 try bufw.writer().writeAll("\n");
259 try bufw.flush();
648 for (parsed.positionals.slice()) |item| {
649 std.debug.print(" - {s}\n", .{item});
650 }
260651}
652
653const Cli = @This();
654const std = @import("std");
655const mem = std.mem;
656const Type = std.builtin.Type;
src/cli_transform.zig created+98
......@@ -0,0 +1,98 @@
1//! Minimal CLI via `zig build transform -- filepath`
2//! Use this only for working on Parse+Transform
3
4const arg_config: Cli = .{
5 .options = &.{
6 .{ .name = "--ast", .desc = "Show AST", .short = 'a' },
7 .{ .name = "--dump-bin", .kind = .string, .desc = "Dump serialized binary to file" },
8 },
9 .positionals = .single,
10};
11
12pub fn main() !u8 {
13 try core.unicode_props.init();
14
15 const use_gpa = @import("builtin").mode == .Debug;
16 var gpa = if (use_gpa)
17 std.heap.GeneralPurposeAllocator(.{
18 .stack_trace_frames = 100,
19 }){}
20 else
21 std.heap.ArenaAllocator.init(std.heap.page_allocator);
22 defer _ = if (use_gpa) gpa.deinit();
23 const alloc = gpa.allocator();
24
25 const program_name, const raw_args = try Cli.programArgsAlloc(alloc, "transform");
26 defer Cli.programArgsFree(alloc, raw_args);
27
28 const args = arg_config.parse(raw_args) catch
29 arg_config.showHelpAndExit(program_name);
30
31 const file_path = args.positionals orelse
32 arg_config.fatalWithHelp(program_name, "missing filepath argument\n", .{});
33
34 const out = core.OutStream.getStderr();
35
36 const code = try std.fs.cwd()
37 .readFileAllocOptions(alloc, file_path, std.math.maxInt(u32), null, @alignOf(u8), 0);
38 defer alloc.free(code);
39
40 var err_data: core.Parse.AstError.WithTrace = undefined;
41 var ast = core.Parse.parse(
42 alloc,
43 code,
44 &err_data,
45 ) catch |err| switch (err) {
46 error.OutOfMemory => return err,
47 error.ParseFail => {
48 var report = try err_data.toReport(alloc);
49 defer report.deinit(alloc);
50
51 report.dump(
52 .{ .single_file = .{
53 .allocator = alloc,
54 .url = file_path,
55 .file = .{ .utf8_source = code },
56 } },
57 out,
58 ) catch {};
59
60 return 1;
61 },
62 };
63 defer ast.deinit(alloc);
64
65 if (args.ast == true) {
66 ast.dumpToWriter(out.writer) catch {};
67 }
68
69 var teg_report: core.Report = undefined;
70 var teg = core.Transform.transform(ast, alloc, &teg_report) catch |err| switch (err) {
71 error.OutOfMemory => return err,
72 error.TransformFail => {
73 defer teg_report.deinit(alloc);
74 teg_report.dump(
75 .{ .single_file = .{
76 .allocator = alloc,
77 .url = file_path,
78 .file = .{ .ast = &ast },
79 } },
80 out,
81 ) catch {};
82 return 1;
83 },
84 };
85 defer teg.deinit(alloc);
86
87 // const file = try std.fs.cwd().createFile("serialized.teg", .{});
88 // defer file.close();
89 // try teg.serialize(.{ .file = file });
90
91 try teg.dumpToWriter(out.writer, out.tty);
92
93 return 0;
94}
95
96const std = @import("std");
97const core = @import("core");
98const Cli = @import("Cli.zig");
src/core/transform_cli.zig deleted-85
......@@ -1,85 +0,0 @@
1//! Minimal CLI via `zig build transform -- filepath`
2//! Use this only for working on Parse+Transform
3
4pub fn main() !u8 {
5 try core.unicode_props.init();
6
7 const use_gpa = @import("builtin").mode == .Debug;
8 var gpa = if (use_gpa)
9 std.heap.GeneralPurposeAllocator(.{
10 .stack_trace_frames = 100,
11 }){}
12 else
13 std.heap.ArenaAllocator.init(std.heap.page_allocator);
14 defer _ = if (use_gpa) gpa.deinit();
15 const alloc = gpa.allocator();
16
17 var args = try std.process.argsWithAllocator(alloc);
18 defer args.deinit();
19
20 _ = args.skip();
21 const file_path = args.next() orelse {
22 std.debug.print("Missing filepath argument\n", .{});
23 return 1;
24 };
25
26 const out = core.OutStream.getStderr();
27
28 const code = try std.fs.cwd()
29 .readFileAllocOptions(alloc, file_path, std.math.maxInt(u32), null, @alignOf(u8), 0);
30 defer alloc.free(code);
31
32 var err_data: core.Parse.AstError.WithTrace = undefined;
33 var ast = core.Parse.parse(
34 alloc,
35 code,
36 &err_data,
37 ) catch |err| switch (err) {
38 error.OutOfMemory => return err,
39 error.ParseFail => {
40 var report = try err_data.toReport(alloc);
41 defer report.deinit(alloc);
42
43 report.dump(
44 .{ .single_file = .{
45 .allocator = alloc,
46 .url = file_path,
47 .file = .{ .utf8_source = code },
48 } },
49 out,
50 ) catch {};
51
52 return 1;
53 },
54 };
55 defer ast.deinit(alloc);
56
57 var teg_report: core.Report = undefined;
58 var teg = core.Transform.transform(ast, alloc, &teg_report) catch |err| switch (err) {
59 error.OutOfMemory => return err,
60 error.TransformFail => {
61 defer teg_report.deinit(alloc);
62 teg_report.dump(
63 .{ .single_file = .{
64 .allocator = alloc,
65 .url = file_path,
66 .file = .{ .ast = &ast },
67 } },
68 out,
69 ) catch {};
70 return 1;
71 },
72 };
73 defer teg.deinit(alloc);
74
75 // const file = try std.fs.cwd().createFile("serialized.teg", .{});
76 // defer file.close();
77 // try teg.serialize(.{ .file = file });
78
79 try teg.dumpToWriter(out.writer, out.tty);
80
81 return 0;
82}
83
84const std = @import("std");
85const core = @import("core");
src/main.zig created+260
......@@ -0,0 +1,260 @@
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.
6
7const have_debug_features = builtin.mode == .Debug;
8const config = @import("config");
9
10pub fn main() !u8 {
11 try tracer.init();
12 defer tracer.deinit();
13 try tracer.init_thread(null);
14 defer tracer.deinit_thread();
15
16 // unicode props is asserted to be initialized before core.Tokenizer is used
17 // this will result in a static page allocation, which is never cleaned up.
18 try core.unicode_props.init();
19
20 const use_gpa = builtin.mode == .Debug;
21 var gpa = if (use_gpa)
22 std.heap.GeneralPurposeAllocator(.{
23 .stack_trace_frames = 100,
24 }){}
25 else
26 std.heap.ArenaAllocator.init(std.heap.page_allocator);
27 var no_leak_checks = false;
28 defer _ = if (use_gpa and !no_leak_checks) gpa.deinit();
29 const alloc = gpa.allocator();
30
31 // Current working directory is required for CLI resolution.
32 var cwd_buf: [std.fs.max_path_bytes]u8 = undefined;
33 const cwd = try std.posix.getcwd(&cwd_buf);
34
35 // Command line argument parsing.
36 var maybe_file_path: ?[]const u8 = null;
37 defer if (maybe_file_path) |path| alloc.free(path);
38 var exe_name: ?[]const u8 = null;
39 defer if (exe_name) |path| alloc.free(path);
40 var dump_ir = false;
41 var dump_state: ?[]const u8 = null;
42 var run = false;
43 var has_errors = false;
44 {
45 var args = try std.process.argsWithAllocator(alloc);
46 defer args.deinit();
47
48 exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli"));
49
50 while (args.next()) |arg| {
51 if (std.mem.startsWith(u8, arg, "-")) {
52 if (have_debug_features) {
53 if (std.mem.eql(u8, arg, "--report-trace")) {
54 core.Report.debug_show_report_trace = true;
55 continue;
56 }
57 if (std.mem.eql(u8, arg, "--no-leak-checks")) {
58 no_leak_checks = true;
59 continue;
60 }
61 }
62
63 if (std.mem.eql(u8, arg, "--dump-ir")) {
64 dump_ir = true;
65 continue;
66 }
67
68 if (std.mem.startsWith(u8, arg, "--dump-state=")) {
69 dump_state = arg["--dump-state=".len..];
70 continue;
71 }
72
73 if (config.have_embedded_quickjs and std.mem.eql(u8, arg, "--run")) {
74 run = true;
75 continue;
76 }
77
78 std.debug.print("error: unknown flag '{s}'\n", .{arg});
79 has_errors = true;
80 break;
81 }
82
83 if (maybe_file_path == null) {
84 maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg });
85 } else {
86 std.debug.print("error: multiple files specified\n", .{});
87 maybe_file_path = null;
88 break;
89 }
90 }
91 }
92
93 if (maybe_file_path == null) has_errors = true;
94 if (has_errors) {
95 std.debug.print("Usage: {s} [flags] <input file>\n", .{exe_name.?});
96 {
97 std.debug.print(
98 // | 60 chars wrap point|
99 \\
100 \\Flags:
101 \\ --dump-ir Dump intermediate representations
102 \\ --dump-state=<path> Dump compiler state to JSON
103 \\
104 , .{});
105 }
106 if (config.have_embedded_quickjs) {
107 std.debug.print(
108 \\ --run Run program using QuickJS
109 \\
110 , .{});
111 }
112 if (have_debug_features) {
113 std.debug.print(
114 \\
115 \\Debug Mode Flags:
116 \\ --report-trace Print stack traces per error
117 \\ --no-leak-checks
118 \\
119 , .{});
120 }
121 std.debug.print("\nLanguage SDK v" ++ config.version_string ++ "\n", .{});
122 return 1;
123 }
124 const file_path = maybe_file_path.?;
125
126 // Execute compiler
127 {
128 var js_backend = core.Js{};
129
130 var comp = try core.Comp.init(.{
131 .gpa = tracer.tracingAllocator(&alloc),
132 .options = .{
133 .dump_ir = dump_ir,
134 },
135 .backend = .{ .js = &js_backend },
136 });
137 defer comp.deinit();
138
139 defer if (dump_state) |path| {
140 dumpState(&comp, path) catch |err| {
141 std.debug.print("Failed to dump compilation state: {}", .{err});
142 };
143 };
144
145 const main_file = try comp.getFile(file_path);
146
147 var fail_report: Report = undefined;
148 const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) {
149 error.TransformFail => {
150 std.debug.print("Transform failed:\n", .{});
151 try fail_report.dump(.initComp(&comp, main_file), comp.out);
152 fail_report.deinit(comp.gpa);
153 return 1;
154 },
155 error.ParseFail => {
156 // Already printed.
157 return 1;
158 },
159 else => return err,
160 };
161
162 if (comp.options.dump_ir) {
163 const stderr = std.io.getStdErr().writer();
164 try teg.dumpToWriter(stderr, null);
165 }
166
167 const entry_point = try comp.enqueueEntryModule(main_file);
168 comp.runAllJobs() catch |err| switch (err) {
169 error.AnalysisFail => {
170 // error is already printed
171 return 1;
172 },
173 else => return err,
174 };
175
176 if (config.have_embedded_quickjs and run) {
177 // Execute with QuickJS
178 var out: std.ArrayListUnmanaged(u8) = .{};
179 defer out.deinit(alloc);
180 try js_backend.link(&comp, entry_point, out.writer(alloc).any());
181 try out.append(alloc, 0);
182 try runWithQuickJs(out.items[0 .. out.items.len - 1 :0]);
183 } else {
184 // Output to standard out
185 const output = std.io.getStdOut().writer();
186 var bufw = std.io.bufferedWriter(output);
187 const writer = bufw.writer();
188 try js_backend.link(&comp, entry_point, writer.any());
189 try bufw.flush();
190 }
191 }
192
193 std.process.cleanExit();
194 return 0;
195}
196
197fn runWithQuickJs(js: [:0]const u8) !void {
198 const qjs = @import("quickjs");
199
200 const runtime = qjs.JS_NewRuntime() orelse
201 return error.UnexpectedError;
202 defer qjs.JS_FreeRuntime(runtime);
203 const js_ctx = qjs.JS_NewContext(runtime) orelse
204 return error.UnexpectedError;
205 defer qjs.JS_FreeContext(js_ctx);
206
207 // QuickJS provides a completely blank global context. It is a non-goal to
208 // be a full runtime, but adding simple globals to align with expectations
209 // is OK.
210
211 // 0 and null is zero process args
212 qjs.js_std_add_helpers(js_ctx, 0, null);
213
214 const result = qjs.JS_Eval(
215 js_ctx,
216 js.ptr,
217 js.len,
218 "test.js",
219 qjs.JS_EVAL_TYPE_GLOBAL,
220 );
221 defer qjs.JS_FreeValue(js_ctx, result);
222
223 if (qjs.JS_IsException(result) != 0) {
224 const exception = qjs.JS_GetException(js_ctx);
225 defer qjs.JS_FreeValue(js_ctx, exception);
226
227 const exception_str = qjs.JS_ToCString(js_ctx, exception);
228 defer qjs.JS_FreeCString(js_ctx, exception_str);
229
230 const slice = std.mem.span(exception_str);
231
232 std.debug.print("Exception: {s}\n", .{slice});
233
234 std.process.exit(1);
235 }
236}
237
238const core = @import("core");
239const Comp = core.Comp;
240const Report = core.Report;
241
242const tracer = @import("tracer");
243pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));
244
245const std = @import("std");
246const builtin = @import("builtin");
247
248fn dumpState(comp: *const Comp, dump_path: []const u8) !void {
249 const file = try std.fs.cwd().createFile(dump_path, .{});
250 defer file.close();
251
252 var bufw = std.io.bufferedWriter(file.writer());
253 var jw = std.json.writeStream(bufw.writer(), .{
254 .whitespace = .indent_2,
255 });
256
257 try comp.jsonStringify(&jw);
258 try bufw.writer().writeAll("\n");
259 try bufw.flush();
260}
test/compiler.zig+27-44
......@@ -5,6 +5,19 @@
55//! structure of these tests, and `zig build test-compiler -- --help` for more
66//! details on the arguments this supports.
77
8const cli_config: Cli = .{
9 .options = &.{
10 .{ .name = "--test-root", .desc = "root directory of all tests", .kind = .string },
11 .{ .name = "--all-logs", .desc = "Always print compiler logs" },
12 .{ .name = "--no-threads", .desc = "Avoid using a thread pool" },
13 .{ .name = "--dump-ir", .desc = "Dump intermediate representations" },
14 },
15 .positionals = .single,
16 .help = .{
17 .usage = "[options...] [test filter]",
18 },
19};
20
821/// This file is included for all behavior tests for the JavaScript backend.
922const helpers =
1023 \\(function (self) {
......@@ -237,50 +250,19 @@ pub fn main() !void {
237250 defer _ = gpa.deinit();
238251 const alloc = gpa.allocator();
239252
240 const args = try std.process.argsAlloc(alloc);
241 defer std.process.argsFree(alloc, args);
242
243 var test_root: []const u8 = "test/behavior";
244 var case_options: CaseOptions = .{};
245 var all_logs = false;
246 var filter: ?[]const u8 = null;
247 defer if (filter) |f| alloc.free(f);
248
249 var argument_error = false;
250
251 for (args[1..]) |full_arg| {
252 if (std.mem.startsWith(u8, full_arg, "--")) {
253 if (getFlag("--test-root", full_arg)) |flag| {
254 test_root = flag;
255 } else if (std.mem.eql(u8, "--dump-ir", full_arg)) {
256 case_options.dump_ir = true;
257 } else if (std.mem.eql(u8, "--all-logs", full_arg)) {
258 all_logs = true;
259 } else if (std.mem.eql(u8, "--sync", full_arg)) {
260 case_options.sync = true;
261 } else {
262 std.debug.print("Unknown argument: {s}\n", .{full_arg});
263 argument_error = true;
264 }
265 } else {
266 if (filter != null) {
267 std.debug.print("Unknown positional argument: {s}\n", .{full_arg});
268 } else {
269 filter = try std.mem.replaceOwned(
270 u8,
271 alloc,
272 full_arg,
273 if (std.fs.path.sep == '/') "\\" else '/',
274 std.fs.path.sep_str,
275 );
276 }
277 }
278 }
253 const program_name = "zig build test-compiler --";
254 _, const args = try Cli.programArgsAlloc(alloc, "");
255 defer Cli.programArgsFree(alloc, args);
279256
280 if (argument_error) {
281 // TODO: print a nice help menu
282 std.process.exit(1);
283 }
257 const opts = cli_config.parse(program_name, args);
258
259 const test_root = opts.test_root orelse "test/behavior";
260 const case_options: CaseOptions = .{
261 .dump_ir = opts.dump_ir orelse false,
262 .sync = opts.no_threads orelse false,
263 };
264 const all_logs = opts.all_logs orelse false;
265 const filter: ?[]const u8 = opts.positionals;
284266
285267 const some_logs = all_logs or args.len > 1;
286268
......@@ -300,7 +282,7 @@ pub fn main() !void {
300282 if (filter) |f| {
301283 std.debug.print("No test cases match filter \"{s}\"\n", .{f});
302284 } else {
303 std.debug.print("No test cases\n", .{});
285 std.debug.print("No test cases in root \"{s}\"", .{test_root});
304286 }
305287 std.process.exit(1);
306288 }
......@@ -424,6 +406,7 @@ fn getFlag(name: []const u8, full_arg: []const u8) ?[]const u8 {
424406const std = @import("std");
425407const Dir = std.fs.Dir;
426408
409const Cli = @import("Cli");
427410const core = @import("core");
428411const qjs = @import("quickjs");
429412