diff --git a/build.zig b/build.zig index de25ba27b57aa2359c760f449487f958c7a52965..18e0ca831f79de13881cd284060a88d42a0c1385 100644 --- a/build.zig +++ b/build.zig @@ -30,6 +30,7 @@ const ArtifactGroup = struct { transform_cli: *Step.Compile, loader_cli: *Step.Compile, + cli_mod: *Module, core_mod: *Module, loader_mod: *Module, config_mod: *Module, @@ -59,14 +60,6 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { break :config_mod config.createModule(); }; - // Cli helper - const cli_mod = b.createModule(.{ - .root_source_file = b.path("src/Cli.zig"), - .optimize = options.optimize, - .target = options.target, - .strip = false, - }); - // Core const core_mod = b.createModule(.{ .root_source_file = b.path("src/core/mod.zig"), @@ -103,10 +96,18 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { const core_mod_tests = createModuleTests(core_mod, options); const loader_mod_tests = createModuleTests(loader_mod, options); + // Cli helper module + const cli_mod = b.createModule(.{ + .root_source_file = b.path("src/Cli.zig"), + .optimize = options.optimize, + .target = options.target, + .strip = false, + }); + // Compiler test CLI const compiler_tests = b.addExecutable(.{ .name = "compiler_tests", - .root_source_file = b.path("test/compiler.zig"), + .root_source_file = b.path("test/compiler_tests.zig"), .target = options.target, .optimize = options.optimize, .use_lld = use_lld, @@ -134,6 +135,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { if (options.config.have_embedded_quickjs) { cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc")); } + cli.root_module.addImport("Cli", cli_mod); // Compiler Dev CLI const compiler_cli = b.addExecutable(.{ @@ -151,6 +153,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { if (options.config.have_embedded_quickjs) { cli.root_module.addImport("quickjs", quickjs.module("quickjs-libc")); } + compiler_cli.root_module.addImport("Cli", cli_mod); const format_cli = b.addExecutable(.{ .name = "format", @@ -165,6 +168,7 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { format_cli.root_module.addImport("format", format_mod); format_cli.root_module.addImport("config", config_mod); format_cli.root_module.addImport("tracer", tracer.module("tracer")); + format_cli.root_module.addImport("Cli", cli_mod); const loader_cli = b.addExecutable(.{ .name = "loader", @@ -175,9 +179,10 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { .use_llvm = use_llvm, .single_threaded = true, }); - format_cli.root_module.addImport("loader", loader_mod); - format_cli.root_module.addImport("config", config_mod); - format_cli.root_module.addImport("tracer", tracer.module("tracer")); + loader_cli.root_module.addImport("loader", loader_mod); + loader_cli.root_module.addImport("config", config_mod); + loader_cli.root_module.addImport("tracer", tracer.module("tracer")); + loader_cli.root_module.addImport("Cli", cli_mod); const transform_cli = b.addExecutable(.{ .name = "transform", @@ -191,12 +196,14 @@ fn createArtifactGroup(b: *Build, options: Options) ArtifactGroup { transform_cli.root_module.addImport("core", core_mod); transform_cli.root_module.addImport("config", config_mod); transform_cli.root_module.addImport("tracer", tracer.module("tracer")); + transform_cli.root_module.addImport("Cli", cli_mod); return .{ .build = b, .opts = options, .cli = cli, + .cli_mod = cli_mod, .compiler_cli = compiler_cli, .format_cli = format_cli, .loader_cli = loader_cli, @@ -399,13 +406,14 @@ pub fn build(b: *Build) !void { b.installArtifact(main.cli); - inline for (.{ + const run_templates = .{ .{ main.cli, "run" }, .{ minimal.compiler_cli, "compiler" }, .{ minimal.format_cli, "format" }, .{ minimal.loader_cli, "loader" }, .{ minimal.transform_cli, "transform" }, - }) |entry| { + }; + inline for (run_templates) |entry| { const artifact, const name = entry; const run_cmd = b.addRunArtifact(artifact); @@ -425,8 +433,12 @@ pub fn build(b: *Build) !void { }) |entry| { const artifact, const name, const description = entry; const run_cmd = b.addRunArtifact(artifact); - if (b.args) |args| { - run_cmd.addArgs(args); + if (artifact == main.compiler_tests) { + if (b.args) |args| { + run_cmd.addArgs(args); + } else { + run_cmd.enableTestRunnerMode(); + } } const run_step = b.step("test-" ++ name, "Test " ++ description); run_step.dependOn(&run_cmd.step); @@ -550,9 +562,14 @@ pub fn build(b: *Build) !void { { _ = b.top_level_steps.swapRemove("uninstall"); // this step does not work - const all_step = b.step("all", "Execute all of the above"); - const steps = b.top_level_steps.values(); - for (steps[0 .. steps.len - 1]) |step| all_step.dependOn(&step.step); + const all_step = b.step("all", "Build everything (used by CI)"); + all_step.dependOn(&b.top_level_steps.get("install").?.step); + all_step.dependOn(&b.top_level_steps.get("package").?.step); + all_step.dependOn(&b.top_level_steps.get("test").?.step); + all_step.dependOn(&b.top_level_steps.get("manual").?.step); + inline for (run_templates) |run| { + all_step.dependOn(&run.@"0".step); + } } } diff --git a/ci b/ci index e721010b36f812794e57fd93ec02f5643b10fea4..619452c747148e93756c6c6bf71355707ced34ad 100755 --- a/ci +++ b/ci @@ -10,7 +10,7 @@ commit_short=$(echo "$commit" | cut -c -9) typos dprint check -./build test package --summary all +./build all --summary new if [ -n "$BUILDKITE" ]; then pushd zig-out/package diff --git a/src/cli_compiler.zig b/src/cli_compiler.zig index 55374767c5dd3648247599a0d1f820849206d0c2..ef054ca3a20d5f4d6ede392a4f2189b4a483fc80 100644 --- a/src/cli_compiler.zig +++ b/src/cli_compiler.zig @@ -130,7 +130,7 @@ pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)) const std = @import("std"); const builtin = @import("builtin"); -const Cli = @import("Cli.zig"); +const Cli = @import("Cli"); fn dumpState(comp: *const Comp, dump_path: []const u8) !void { const file = try std.fs.cwd().createFile(dump_path, .{}); diff --git a/src/cli_transform.zig b/src/cli_transform.zig index 76200d6457a22fefd34076def4e44cfe3c58c97e..501353608d31b9e0e2523c4a608263a56929a8f9 100644 --- a/src/cli_transform.zig +++ b/src/cli_transform.zig @@ -94,4 +94,4 @@ pub fn main() !u8 { const std = @import("std"); const core = @import("core"); -const Cli = @import("Cli.zig"); +const Cli = @import("Cli"); diff --git a/src/main.zig b/src/main.zig index 5311b43e52c814f5079d392495488fca9408db44..2a2c61c5049c039497e9d8693d30c3871f2970cb 100644 --- a/src/main.zig +++ b/src/main.zig @@ -434,7 +434,7 @@ const tracer = @import("tracer"); pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); const builtin = @import("builtin"); -const Cli = @import("Cli.zig"); +const Cli = @import("Cli"); fn dumpState(comp: *const Comp, dump_path: []const u8) !void { const file = try std.fs.cwd().createFile(dump_path, .{}); diff --git a/test/compiler.zig b/test/compiler.zig deleted file mode 100644 index d5b1cd5ca6f55e1142232dcf922325538513cf1f..0000000000000000000000000000000000000000 --- a/test/compiler.zig +++ /dev/null @@ -1,414 +0,0 @@ -//! This program executes compiler tests found in this directory. Additionally, -//! it can be run as a standalone program to compile and execute a single test. -//! -//! See the "Testing" section in `architecture.md` for more information on the -//! structure of these tests, and `zig build test-compiler -- --help` for more -//! details on the arguments this supports. - -const cli_config: Cli = .{ - .options = &.{ - .{ .name = "--test-root", .desc = "root directory of all tests", .kind = .string }, - .{ .name = "--all-logs", .desc = "Always print compiler logs" }, - .{ .name = "--no-threads", .desc = "Avoid using a thread pool" }, - .{ .name = "--dump-ir", .desc = "Dump intermediate representations" }, - }, - .positionals = .single, - .help = .{ - .usage = "[options...] [test filter]", - }, -}; - -/// This file is included for all behavior tests for the JavaScript backend. -const helpers = - \\(function (self) { - \\ function assert(condition) { - \\ if (!condition) { - \\ throw new Error("Assertion failure"); - \\ } - \\ } - \\ - \\ function assertEqual(actual, expected) { - \\ if (actual !== expected) { - \\ var err; - \\ try { - \\ err = new Error("Expected " + expected + ", but got " + actual); - \\ } catch (e) { - \\ err = new Error("Assertion failure"); - \\ } - \\ throw err; - \\ } - \\ } - \\ - \\ function print(str) { - \\ console.log(str); - \\ } - \\ - \\ self.assert = assert; - \\ self.assertEqual = assertEqual; - \\ self.print = print; - \\})(typeof globalThis !== "undefined" ? globalThis : this); - \\ -; - -const CaseOptions = struct { - dump_ir: bool = false, - sync: bool = false, -}; - -const Case = struct { - path: []const u8, - display: []const u8, - success: bool = false, - logs: []const u8 = "", - failure: ?struct { - err: anyerror, - stack: core.StoredTrace, - } = null, - - const Kind = enum { - behavior, - analysis, - transform_error, - }; -}; - -fn runTestCase( - backing_allocator: std.mem.Allocator, - file_path: []const u8, - case_options: CaseOptions, - out: std.io.AnyWriter, -) !void { - var gpa = std.heap.GeneralPurposeAllocator(.{ - .stack_trace_frames = 100, - }){ .backing_allocator = backing_allocator }; - const alloc = gpa.allocator(); - - var backend = core.Js{}; - var comp = try core.Comp.init(.{ - .gpa = tracer.tracingAllocator(&alloc), - .options = .{ .dump_ir = case_options.dump_ir }, - .backend = .{ .js = &backend }, - .out = .{ .writer = out, .tty = .escape_codes }, - }); - - const main_file = try comp.getFile(file_path); - - var fail_report: core.Report = undefined; - const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) { - error.TransformFail => { - try out.print("Transform failed:\n", .{}); - try fail_report.dump(.{ .comp = .{ .comp = &comp, .file = main_file } }, comp.out); - return error.TransformFail; - }, - error.ParseFail => { - // Already printed. - return error.ParseFail; - }, - else => return err, - }; - - if (case_options.dump_ir) { - try teg.dumpToWriter(out, .escape_codes); - } - - const entry_point = try comp.enqueueEntryModule(main_file); - try comp.runAllJobs(); - - if (comp.temp_flag_had_errors) - return error.SemaErrors; - - if (teg.report.warnings.len > 0) - return error.TegWarnings; - - var js_out: std.ArrayListUnmanaged(u8) = .{}; - defer js_out.deinit(alloc); - - try js_out.appendSlice(alloc, helpers); - try js_out.appendSlice(alloc, "\n\n(function() {\n\n"); - - const start_i = js_out.items.len; - try backend.link(&comp, entry_point, js_out.writer(alloc).any()); - - try out.writeAll("--- JS CODE ---\n"); - try out.writeAll(js_out.items[start_i..]); - try out.writeAll("--- JS CODE ---\n"); - - if (isOnlyComments(js_out.items[start_i..])) { - try out.writeAll("Behavior tests should output code!"); - return error.EmptyFile; - } - - try js_out.appendSlice(alloc, "\n\n})();"); - try js_out.append(alloc, 0); - - const runtime = qjs.JS_NewRuntime() orelse - return error.UnexpectedError; - defer qjs.JS_FreeRuntime(runtime); - const js_ctx = qjs.JS_NewContext(runtime) orelse - return error.UnexpectedError; - defer qjs.JS_FreeContext(js_ctx); - - const result = qjs.JS_Eval( - js_ctx, - js_out.items.ptr, - js_out.items.len - 1, - "test.js", - qjs.JS_EVAL_TYPE_GLOBAL, - ); - defer qjs.JS_FreeValue(js_ctx, result); - - if (qjs.JS_IsException(result) != 0) { - const exception = qjs.JS_GetException(js_ctx); - defer qjs.JS_FreeValue(js_ctx, exception); - - const exception_str = qjs.JS_ToCString(js_ctx, exception); - defer qjs.JS_FreeCString(js_ctx, exception_str); - - const slice = std.mem.span(exception_str); - - try out.print("Exception: {s}\n", .{slice}); - - return error.RuntimeFailed; - } -} - -fn isOnlyComments(input: []const u8) bool { - var rest = input; - while (rest.len > 0) { - if (rest.len == 1) return false; - if (rest[0] == '/' and rest[1] == '/') { - rest = rest[std.mem.indexOfScalar(u8, rest, '\n') orelse return true ..]; - } else { - return false; - } - } - return true; -} - -const threadlocal_state = struct { - threadlocal var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); -}; - -pub fn runTestCaseWrap( - gpa: std.mem.Allocator, - case: *Case, - case_options: CaseOptions, - progress: std.Progress.Node, -) void { - const node = progress.start(case.display, 0); - defer node.end(); - - const alloc = threadlocal_state.arena.allocator(); - defer _ = threadlocal_state.arena.reset(.{ .retain_with_limit = std.mem.page_size * 8 }); - - var logs: std.ArrayListUnmanaged(u8) = .{}; - defer logs.deinit(alloc); - - defer case.logs = gpa.dupe(u8, logs.items) catch - "[logs failed to allocate]"; - - runTestCase( - alloc, - case.path, - case_options, - if (case_options.sync) - std.io.getStdErr().writer().any() - else - logs.writer(alloc).any(), - ) catch |err| { - const stack = switch (err) { - error.TransformFail, - error.ParseFail, - error.TegWarnings, - error.SemaErrors, - error.AnalysisFail, - => core.StoredTrace.empty, - - else => core.StoredTrace.from(@errorReturnTrace()), - }; - - case.failure = .{ - .err = err, - .stack = stack, - }; - - return; - }; - - case.success = true; -} - -pub fn main() !void { - // unicode props is asserted to be initialized before core.Tokenizer is used - // this will result in a static page allocation, which is never cleaned up. - try core.unicode_props.init(); - var gpa = std.heap.GeneralPurposeAllocator(.{ - .stack_trace_frames = 100, - .thread_safe = true, - }){}; - - defer _ = gpa.deinit(); - const alloc = gpa.allocator(); - - const program_name = "zig build test-compiler --"; - _, const args = try Cli.programArgsAlloc(alloc, ""); - defer Cli.programArgsFree(alloc, args); - - const opts = cli_config.parse(program_name, args); - - const test_root = opts.test_root orelse "test/behavior"; - const case_options: CaseOptions = .{ - .dump_ir = opts.dump_ir orelse false, - .sync = opts.no_threads orelse false, - }; - const all_logs = opts.all_logs orelse false; - const filter: ?[]const u8 = opts.positionals; - - const some_logs = all_logs or args.len > 1; - - const root = try std.fs.cwd().openDir(test_root, .{ .iterate = true }); - - var test_cases: std.ArrayListUnmanaged(Case) = .{}; - defer test_cases.deinit(alloc); - defer for (test_cases.items) |test_case| { - alloc.free(test_case.path); - }; - - var path_buf: std.BoundedArray(u8, std.fs.max_path_bytes) = .{}; - path_buf.len = @intCast((try std.os.getFdPath(root.fd, &path_buf.buffer)).len); - try iterateTestCases(alloc, root, path_buf.len, &path_buf, &test_cases, filter); - - if (test_cases.items.len == 0) { - if (filter) |f| { - std.debug.print("No test cases match filter \"{s}\"\n", .{f}); - } else { - std.debug.print("No test cases in root \"{s}\"", .{test_root}); - } - std.process.exit(1); - } - - const progress = std.Progress.start(.{ .root_name = "Behavior Tests" }); - defer progress.end(); - progress.setEstimatedTotalItems(test_cases.items.len); - - var wg: std.Thread.WaitGroup = .{}; - - var pool: std.Thread.Pool = undefined; - if (!case_options.sync) try pool.init(.{ .allocator = alloc }); - defer if (!case_options.sync) pool.deinit(); - - for (test_cases.items) |*test_case| { - const call_args = .{ - alloc, - test_case, - case_options, - progress, - }; - - if (case_options.sync) { - @call(.auto, runTestCaseWrap, call_args); - } else { - pool.spawnWg(&wg, runTestCaseWrap, call_args); - } - } - - wg.wait(); - - var num_pass: usize = 0; - for (test_cases.items) |*case| { - if (case.success) { - num_pass += 1; - if (some_logs) { - std.debug.print("PASS: {s}\n", .{case.display}); - } - if (all_logs) { - std.debug.print("{s}", .{case.logs}); - } - } else if (case.failure) |*fail| { - std.debug.print("FAIL: {s}\n", .{case.display}); - std.debug.print("{s}", .{case.logs}); - if (fail.stack.index > 0) { - std.debug.print("Error return trace:\n", .{}); - std.debug.dumpStackTrace(fail.stack.trace()); - } - } else { - // success was never set true but failure was left null - // this means runTestCaseWrap was never called - std.debug.print("INVALID STATE: {s}\n", .{case.path}); - } - if (case.logs.len > 0) - alloc.free(case.logs); - } - - if (some_logs) { - std.debug.print("Complete: {d} PASS / {d} TOTAL\n", .{ num_pass, test_cases.items.len }); - } -} - -fn iterateTestCases( - alloc: std.mem.Allocator, - root: Dir, - root_path_len: usize, - path_buf: *std.BoundedArray(u8, std.fs.max_path_bytes), - test_cases: *std.ArrayListUnmanaged(Case), - filter: ?[]const u8, -) !void { - var it = root.iterate(); - while (try it.next()) |entry| { - switch (entry.kind) { - .file => { - if (entry.name.len > 0 and entry.name[0] == '.') - continue; - - const path = try std.fs.path.join(alloc, &.{ - path_buf.slice(), - entry.name, - }); - - if (filter) |f| { - if (std.mem.indexOf(u8, path, f) == null) { - alloc.free(path); - continue; - } - } - - try test_cases.append(alloc, .{ - .path = path, - .display = path[root_path_len + 1 ..], - }); - }, - .directory => { - const top = path_buf.len; - defer path_buf.len = top; - - try path_buf.append(std.fs.path.sep); - try path_buf.appendSlice(entry.name); - - const sub_dir = try root.openDir(entry.name, .{ .iterate = true }); - try iterateTestCases(alloc, sub_dir, root_path_len, path_buf, test_cases, filter); - }, - else => return error.UnsupportedFileType, - } - } -} - -fn getFlag(name: []const u8, full_arg: []const u8) ?[]const u8 { - if (full_arg.len > name.len + 1 and - full_arg[name.len] == '=' and - std.mem.startsWith(u8, name, full_arg)) - { - return full_arg[name.len + 1 ..]; - } - - return null; -} - -const std = @import("std"); -const Dir = std.fs.Dir; - -const Cli = @import("Cli"); -const core = @import("core"); -const qjs = @import("quickjs"); - -const tracer = @import("tracer"); -pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode)); diff --git a/test/compiler_tests.zig b/test/compiler_tests.zig new file mode 100644 index 0000000000000000000000000000000000000000..f643ca113822df469b333379aaf3f7d9201b523d --- /dev/null +++ b/test/compiler_tests.zig @@ -0,0 +1,508 @@ +//! This program executes compiler tests found in this directory. Additionally, +//! it can be run as a standalone program to compile and execute a single test. +//! +//! See the "Testing" section in `architecture.md` for more information on the +//! structure of these tests, and `zig build test-compiler -- --help` for more +//! details on the arguments this supports. + +const cli_config: Cli = .{ + .options = &.{ + .{ .name = "--test-root", .desc = "root directory of all tests", .kind = .string }, + .{ .name = "--all-logs", .desc = "Always print compiler logs" }, + .{ .name = "--no-threads", .desc = "Avoid using a thread pool" }, + .{ .name = "--dump-ir", .desc = "Dump intermediate representations" }, + + .{ .name = "--listen", .desc = "Integrate with the Zig Build System IPC", .kind = .string }, + .{ .name = "--seed", .desc = "Set random number seed (unused)", .kind = .string }, + .{ .name = "--cache-dir", .desc = "Set cache dir (unused)", .kind = .string }, + }, + .positionals = .single, + .help = .{ + .usage = "[options...] [test filter]", + }, +}; + +/// This file is included for all behavior tests for the JavaScript backend. +const helpers = + \\(function (self) { + \\ function assert(condition) { + \\ if (!condition) { + \\ throw new Error("Assertion failure"); + \\ } + \\ } + \\ + \\ function assertEqual(actual, expected) { + \\ if (actual !== expected) { + \\ var err; + \\ try { + \\ err = new Error("Expected " + expected + ", but got " + actual); + \\ } catch (e) { + \\ err = new Error("Assertion failure"); + \\ } + \\ throw err; + \\ } + \\ } + \\ + \\ function print(str) { + \\ console.log(str); + \\ } + \\ + \\ self.assert = assert; + \\ self.assertEqual = assertEqual; + \\ self.print = print; + \\})(typeof globalThis !== "undefined" ? globalThis : this); + \\ +; + +const CaseOptions = struct { + dump_ir: bool = false, + sync: bool = false, +}; + +const Case = struct { + path: []const u8, + display: []const u8, + success: bool = false, + logs: []const u8 = "", + failure: ?struct { + err: anyerror, + stack: core.StoredTrace, + } = null, + + const Kind = enum { + behavior, + analysis, + transform_error, + }; +}; + +fn runTestCase( + backing_allocator: std.mem.Allocator, + file_path: []const u8, + case_options: CaseOptions, + out: std.io.AnyWriter, +) !void { + var gpa = std.heap.GeneralPurposeAllocator(.{ + .stack_trace_frames = 100, + }){ .backing_allocator = backing_allocator }; + const alloc = gpa.allocator(); + + var backend = core.Js{}; + var comp = try core.Comp.init(.{ + .gpa = tracer.tracingAllocator(&alloc), + .options = .{ .dump_ir = case_options.dump_ir }, + .backend = .{ .js = &backend }, + .out = .{ .writer = out, .tty = .escape_codes }, + }); + + const main_file = try comp.getFile(file_path); + + var fail_report: core.Report = undefined; + const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) { + error.TransformFail => { + try out.print("Transform failed:\n", .{}); + try fail_report.dump(.{ .comp = .{ .comp = &comp, .file = main_file } }, comp.out); + return error.TransformFail; + }, + error.ParseFail => { + // Already printed. + return error.ParseFail; + }, + else => return err, + }; + + if (case_options.dump_ir) { + try teg.dumpToWriter(out, .escape_codes); + } + + const entry_point = try comp.enqueueEntryModule(main_file); + try comp.runAllJobs(); + + if (comp.temp_flag_had_errors) + return error.SemaErrors; + + if (teg.report.warnings.len > 0) + return error.TegWarnings; + + var js_out: std.ArrayListUnmanaged(u8) = .{}; + defer js_out.deinit(alloc); + + try js_out.appendSlice(alloc, helpers); + try js_out.appendSlice(alloc, "\n\n(function() {\n\n"); + + const start_i = js_out.items.len; + try backend.link(&comp, entry_point, js_out.writer(alloc).any()); + + try out.writeAll("--- COMPILED CODE ---\n"); + try out.writeAll(js_out.items[start_i..]); + try out.writeAll("--- RUNTIME OUTPUT ---\n"); + + if (isOnlyComments(js_out.items[start_i..])) { + try out.writeAll("Behavior tests should output code!"); + return error.EmptyFile; + } + + try js_out.appendSlice(alloc, "\n\n})();"); + try js_out.append(alloc, 0); + + const runtime = qjs.JS_NewRuntime() orelse + return error.UnexpectedError; + defer qjs.JS_FreeRuntime(runtime); + const js_ctx = qjs.JS_NewContext(runtime) orelse + return error.UnexpectedError; + defer qjs.JS_FreeContext(js_ctx); + + const result = qjs.JS_Eval( + js_ctx, + js_out.items.ptr, + js_out.items.len - 1, + "test.js", + qjs.JS_EVAL_TYPE_GLOBAL, + ); + defer qjs.JS_FreeValue(js_ctx, result); + + if (qjs.JS_IsException(result) != 0) { + const exception = qjs.JS_GetException(js_ctx); + defer qjs.JS_FreeValue(js_ctx, exception); + + const exception_str = qjs.JS_ToCString(js_ctx, exception); + defer qjs.JS_FreeCString(js_ctx, exception_str); + + const slice = std.mem.span(exception_str); + + try out.print("Exception: {s}\n", .{slice}); + + return error.RuntimeFailed; + } +} + +fn isOnlyComments(input: []const u8) bool { + var rest = input; + while (rest.len > 0) { + if (rest.len == 1) return false; + if (rest[0] == '/' and rest[1] == '/') { + rest = rest[std.mem.indexOfScalar(u8, rest, '\n') orelse return true ..]; + } else { + return false; + } + } + return true; +} + +const threadlocal_state = struct { + threadlocal var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +}; + +pub fn runTestCaseWrap( + gpa: std.mem.Allocator, + case: *Case, + case_options: CaseOptions, + progress: std.Progress.Node, +) void { + const node = progress.start(case.display, 0); + defer node.end(); + + const alloc = threadlocal_state.arena.allocator(); + defer _ = threadlocal_state.arena.reset(.{ .retain_with_limit = std.mem.page_size * 8 }); + + var logs: std.ArrayListUnmanaged(u8) = .{}; + defer logs.deinit(alloc); + + defer case.logs = gpa.dupe(u8, logs.items) catch + "[logs failed to allocate]"; + + runTestCase( + alloc, + case.path, + case_options, + if (case_options.sync) + std.io.getStdErr().writer().any() + else + logs.writer(alloc).any(), + ) catch |err| { + const stack = switch (err) { + error.TransformFail, + error.ParseFail, + error.TegWarnings, + error.SemaErrors, + error.AnalysisFail, + => core.StoredTrace.empty, + + else => core.StoredTrace.from(@errorReturnTrace()), + }; + + case.failure = .{ + .err = err, + .stack = stack, + }; + + return; + }; + + case.success = true; +} + +pub fn main() !void { + // unicode props is asserted to be initialized before core.Tokenizer is used + // this will result in a static page allocation, which is never cleaned up. + try core.unicode_props.init(); + var gpa = std.heap.GeneralPurposeAllocator(.{ + .stack_trace_frames = 100, + .thread_safe = true, + }){}; + + defer _ = gpa.deinit(); + const alloc = gpa.allocator(); + + const program_name = "zig build test-compiler --"; + _, const args = try Cli.programArgsAlloc(alloc, ""); + defer Cli.programArgsFree(alloc, args); + + const opts = cli_config.parse(program_name, args); + + const root_path = opts.test_root orelse "test/behavior"; + const case_options: CaseOptions = .{ + .dump_ir = opts.dump_ir orelse false, + .sync = opts.no_threads orelse false, + }; + const all_logs = opts.all_logs orelse false; + const filter: ?[]const u8 = opts.positionals; + + const some_logs = all_logs or args.len > 1; + + const root = try std.fs.cwd().openDir(root_path, .{ .iterate = true }); + + var test_cases: std.ArrayListUnmanaged(Case) = .{}; + defer test_cases.deinit(alloc); + defer for (test_cases.items) |test_case| { + alloc.free(test_case.path); + }; + + var path_buf: std.BoundedArray(u8, std.fs.max_path_bytes) = .{}; + path_buf.len = @intCast((try std.os.getFdPath(root.fd, &path_buf.buffer)).len); + try iterateTestCases(alloc, root, path_buf.len, &path_buf, &test_cases, filter); + + if (test_cases.items.len == 0) { + if (filter) |f| { + std.debug.print("No test cases match filter \"{s}\"\n", .{f}); + } else { + std.debug.print("No test cases in root \"{s}\"", .{root_path}); + } + std.process.exit(1); + } + + const progress = std.Progress.start(.{ .root_name = "Behavior Tests" }); + defer progress.end(); + progress.setEstimatedTotalItems(test_cases.items.len); + + var wg: std.Thread.WaitGroup = .{}; + + var pool: std.Thread.Pool = undefined; + if (!case_options.sync) try pool.init(.{ .allocator = alloc }); + defer if (!case_options.sync) pool.deinit(); + + for (test_cases.items) |*test_case| { + const call_args = .{ + alloc, + test_case, + case_options, + progress, + }; + + if (case_options.sync) { + @call(.auto, runTestCaseWrap, call_args); + } else { + pool.spawnWg(&wg, runTestCaseWrap, call_args); + } + } + + wg.wait(); + + if (opts.listen) |listen| { + try serveResults(gpa.allocator(), listen, test_cases.items); + return; + } + + var num_pass: usize = 0; + for (test_cases.items) |*case| { + if (case.success) { + num_pass += 1; + if (some_logs) { + std.debug.print("PASS: {s}\n", .{case.display}); + } + if (all_logs) { + std.debug.print("{s}", .{case.logs}); + } + } else if (case.failure) |*fail| { + std.debug.print("FAIL: {s}\n", .{case.display}); + std.debug.print("{s}", .{case.logs}); + if (fail.stack.index > 0) { + std.debug.print("Error return trace:\n", .{}); + std.debug.dumpStackTrace(fail.stack.trace()); + } + } else { + // success was never set true but failure was left null + // this means runTestCaseWrap was never called + std.debug.print("INVALID STATE: {s}\n", .{case.path}); + } + if (case.logs.len > 0) + alloc.free(case.logs); + } + + if (some_logs) { + std.debug.print("Complete: {d} PASS / {d} TOTAL\n", .{ num_pass, test_cases.items.len }); + } +} + +const builtin = @import("builtin"); + +var log_err_count: usize = 0; +var fba_buffer: [8192]u8 = undefined; +var fba = std.heap.FixedBufferAllocator.init(&fba_buffer); + +/// This is the minimum amount of work to report to the Zig Build System the +/// results of the unit tests. Instead of running tests on-demand, it runs +/// them in a thread-pool, then reports the results. +fn serveResults(gpa: std.mem.Allocator, listen: []const u8, finished_cases: []Case) !void { + if (!std.mem.eql(u8, listen, "-")) @panic("Server only supports --listen=-"); + + var server = try std.zig.Server.init(.{ + .gpa = fba.allocator(), + .in = std.io.getStdIn(), + .out = std.io.getStdOut(), + .zig_version = builtin.zig_version_string, + }); + defer server.deinit(); + + while (true) { + const hdr = try server.receiveMessage(); + switch (hdr.tag) { + .exit => { + return std.process.exit(0); + }, + .query_test_metadata => { + var string_bytes: std.ArrayListUnmanaged(u8) = .{}; + defer string_bytes.deinit(gpa); + try string_bytes.append(gpa, 0); // Reserve 0 for null. + + const names = try gpa.alloc(u32, finished_cases.len); + defer gpa.free(names); + const expected_panic_msgs = try gpa.alloc(u32, finished_cases.len); + defer gpa.free(expected_panic_msgs); + + for (finished_cases, names, expected_panic_msgs) |case, *name, *expected_panic_msg| { + name.* = @as(u32, @intCast(string_bytes.items.len)); + const prefix = "compiler test: "; + try string_bytes.ensureUnusedCapacity(gpa, prefix.len + case.display.len + 1); + string_bytes.appendSliceAssumeCapacity(prefix); + string_bytes.appendSliceAssumeCapacity(case.display); + string_bytes.appendAssumeCapacity(0); + expected_panic_msg.* = 0; + } + + try server.serveTestMetadata(.{ + .names = names, + .expected_panic_msgs = expected_panic_msgs, + .string_bytes = string_bytes.items, + }); + }, + .run_test => { + log_err_count = 0; + const index = try server.receiveBody_u32(); + const case = &finished_cases[index]; + if (case.failure) |*fail| { + std.debug.print("{s}\n{s}", .{ @errorName(fail.err), case.logs }); + if (fail.stack.index > 0) { + std.debug.print("Error return trace:\n", .{}); + std.debug.dumpStackTrace(fail.stack.trace()); + } + } + try server.serveTestResults(.{ + .index = index, + .flags = .{ + .fail = case.failure != null, + .skip = false, + .leak = false, + .fuzz = false, + .log_err_count = std.math.lossyCast( + @TypeOf(@as(std.zig.Server.Message.TestResults.Flags, undefined).log_err_count), + log_err_count, + ), + }, + }); + }, + else => { + std.debug.print("unsupported message: {x}\n", .{@intFromEnum(hdr.tag)}); + std.process.exit(1); + }, + } + } +} + +fn iterateTestCases( + alloc: std.mem.Allocator, + root: Dir, + root_path_len: usize, + path_buf: *std.BoundedArray(u8, std.fs.max_path_bytes), + test_cases: *std.ArrayListUnmanaged(Case), + filter: ?[]const u8, +) !void { + var it = root.iterate(); + while (try it.next()) |entry| { + switch (entry.kind) { + .file => { + if (entry.name.len > 0 and entry.name[0] == '.') + continue; + + const path = try std.fs.path.join(alloc, &.{ + path_buf.slice(), + entry.name, + }); + + if (filter) |f| { + if (std.mem.indexOf(u8, path, f) == null) { + alloc.free(path); + continue; + } + } + + try test_cases.append(alloc, .{ + .path = path, + .display = path[root_path_len + 1 ..], + }); + }, + .directory => { + const top = path_buf.len; + defer path_buf.len = top; + + try path_buf.append(std.fs.path.sep); + try path_buf.appendSlice(entry.name); + + const sub_dir = try root.openDir(entry.name, .{ .iterate = true }); + try iterateTestCases(alloc, sub_dir, root_path_len, path_buf, test_cases, filter); + }, + else => return error.UnsupportedFileType, + } + } +} + +fn getFlag(name: []const u8, full_arg: []const u8) ?[]const u8 { + if (full_arg.len > name.len + 1 and + full_arg[name.len] == '=' and + std.mem.startsWith(u8, name, full_arg)) + { + return full_arg[name.len + 1 ..]; + } + + return null; +} + +const std = @import("std"); +const Dir = std.fs.Dir; + +const Cli = @import("Cli"); +const core = @import("core"); +const qjs = @import("quickjs"); + +const tracer = @import("tracer"); +pub const tracer_impl = @field(tracer, @tagName(@import("config").tracing_mode));