From 84d69ae4e43f53b2b0b243cff4f1f67303825b4b Mon Sep 17 00:00:00 2001 From: dave caruso Date: Sat, 27 Jul 2024 22:14:55 -0700 Subject: [PATCH] chore: add behavior test runner (#82) all new compiler behavior should be tested via `zig build test-behavior` --- CHANGELOG.md | 2 + build.zig | 192 +++++++++----- build.zig.zon | 6 + src/BuildConfig.zig | 9 + src/cli.zig | 186 ++++++++++++- src/core/Air.Dump.zig | 3 +- src/core/Air.zig | 8 +- src/core/Comp.zig | 7 +- src/core/InternPool.zig | 12 + src/core/Sema.zig | 20 +- src/core/StoredTrace.zig | 4 +- src/core/Teg.Dump.zig | 3 +- src/core/Teg.zig | 4 +- src/core/js/NodeGen.zig | 8 +- src/core/main.zig | 170 ------------ src/core/mod.zig | 35 +++ src/deps/quickjs/build.zig | 128 +++++++++ src/deps/quickjs/build.zig.zon | 16 ++ src/lsp/lsp.zig | 41 --- src/manual/generate-man.zig | 461 ++++++++++++++++++++++++++++----- test/behavior.zig | 393 ++++++++++++++++++++++++++++ test/behavior/arithmetic | 67 +++++ 22 files changed, 1401 insertions(+), 374 deletions(-) create mode 100644 src/BuildConfig.zig delete mode 100644 src/core/main.zig create mode 100644 src/deps/quickjs/build.zig create mode 100644 src/deps/quickjs/build.zig.zon delete mode 100644 src/lsp/lsp.zig create mode 100644 test/behavior.zig create mode 100644 test/behavior/arithmetic diff --git a/CHANGELOG.md b/CHANGELOG.md index b00edae46ac5474bd10bce56c3d4e642612738ea..c13fab77916a4b75c6e920299304770e20fa9155 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ - fix: crash when printing unused struct declaration error - fix: provide array result type to sema - fix: crash when analyzing an empty array without a result type +- fix: binary operands with float and float would not resolve +- fix: assertion false-positive when using an extern global more than once ## 0.1.5 diff --git a/build.zig b/build.zig index 8dae28f45c61cae865f17e59372e979ea93702a7..0cfbc52b92c39b96a987a587c65faf05ec060d55 100644 --- a/build.zig +++ b/build.zig @@ -1,12 +1,9 @@ -const std = @import("std"); -const Build = std.Build; -const Target = std.Target; -const OsVersion = Target.Query.OsVersion; +//! See `architecture.md` for a codebase overview, and `zig-out/doc/README.html` +//! for information on the project. -pub fn build(b: *std.Build) !void { +pub fn build(b: *Build) !void { b.reference_trace = 20; - const native_target = b.resolveTargetQuery(.{}); const target = b.standardTargetOptions(.{}); // I disagree with how the standard optimize flag works @@ -28,6 +25,7 @@ pub fn build(b: *std.Build) !void { break :brk .ReleaseSafe; }, }; + const tracing = b.option( TracingMode, "trace", @@ -52,14 +50,26 @@ pub fn build(b: *std.Build) !void { break :version std.SemanticVersion.parse(version_text) catch unreachable; }; - const native_debug = buildWithTarget(b, native_target, .Debug, .none, version, no_llvm); + const js_runtime: PathOrString = if (b.option( + []const u8, + "js_runtime", + "JavaScript runtime to use during behavior tests and manual generation", + )) |path| + .{ .string = path } + else + // Default to building QuickJS + .{ .path = b.dependency("quickjs", .{ + .target = b.graph.host, + .optimize = .ReleaseSafe, + }).artifact("qjs").getEmittedBin() }; + + const native_debug = buildWithTarget(b, b.graph.host, .Debug, .none, version, no_llvm); const main = buildWithTarget(b, target, optimize, tracing, version, no_llvm); - b.installArtifact(main.compiler); + b.installArtifact(main.cli(b, .{})); inline for (.{ - .{ main.compiler, "compiler" }, - .{ main.lsp, "lsp" }, + .{ main.cli(b, BuildConfig.minimal), "compiler" }, }) |entry| { const artifact, const name = entry; @@ -73,16 +83,17 @@ pub fn build(b: *std.Build) !void { const test_all = b.step("test", "Run all tests"); inline for (.{ - .{ main.core_tests, "core" }, + .{ main.core_tests, "core", "core module" }, + .{ main.behavior_tests, "behavior", "compiler behavior" }, }) |entry| { - const artifact, const name = entry; + const artifact, const name, const description = entry; const run_cmd = b.addRunArtifact(artifact); - run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { run_cmd.addArgs(args); } - const run_step = b.step("test-" ++ name, "Test " ++ name ++ " module"); + const run_step = b.step("test-" ++ name, "Test " ++ description); run_step.dependOn(&run_cmd.step); + run_step.dependOn(&b.addInstallArtifact(artifact, .{}).step); test_all.dependOn(run_step); } @@ -90,19 +101,23 @@ pub fn build(b: *std.Build) !void { const manual_generator = b.addExecutable(.{ .name = "generate-man", .root_source_file = b.path("src/manual/generate-man.zig"), - .target = native_target, + .target = b.graph.host, .optimize = .Debug, + .link_libc = true, }); manual_generator.root_module.addImport("build_options", native_debug.opts_mod); manual_generator.root_module.addImport("core", native_debug.core); const manual_run = b.addRunArtifact(manual_generator); - manual_run.addFileArg(native_debug.compiler.getEmittedBin()); + manual_run.addFileArg(native_debug.cli(b, BuildConfig.minimal).getEmittedBin()); + switch (js_runtime) { + .path => |path| manual_run.addFileArg(path), + .string => |str| manual_run.addArg(str), + } manual_run.addFileArg(b.path("doc/language-manual.md")); manual_run.addFileArg(b.path("src/manual/template.html")); const manual_out = manual_run.addOutputFileArg("language-reference.html"); const install_manual = b.addInstallFile(manual_out, "doc/README.html"); b.step("manual", "build language reference").dependOn(&install_manual.step); - b.install_tls.step.dependOn(&install_manual.step); // const wasm_api = b.addExecutable(.{ // .name = "compiler", @@ -129,41 +144,43 @@ pub fn build(b: *std.Build) !void { const win_version: OsVersion = .{ .windows = .win7 }; inline for (.{ - b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl, .os_version_min = linux_version, .cpu_model = .baseline }), - b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl, .os_version_min = linux_version, .cpu_model = .baseline }), - b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .macos, .os_version_min = mac_version, .cpu_model = .baseline }), - b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .macos, .os_version_min = mac_version, .cpu_model = .baseline }), - b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .windows, .os_version_min = win_version, .abi = .gnu, .cpu_model = .baseline }), - b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .windows, .os_version_min = win_version, .abi = .gnu, .cpu_model = .baseline }), + b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .linux, .abi = .musl, .os_version_min = linux_version }), + b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .linux, .abi = .musl, .os_version_min = linux_version }), + b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .macos, .os_version_min = mac_version }), + b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .macos, .os_version_min = mac_version }), + b.resolveTargetQuery(.{ .cpu_arch = .x86_64, .os_tag = .windows, .os_version_min = win_version, .abi = .gnu }), + b.resolveTargetQuery(.{ .cpu_arch = .aarch64, .os_tag = .windows, .os_version_min = win_version, .abi = .gnu }), }) |cross_target| { const name = std.fmt.allocPrint( - b.allocator, + b.graph.arena, "{s}-{s}", .{ @tagName(cross_target.result.cpu.arch), @tagName(cross_target.result.os.tag), }, ) catch @panic("OOM"); + _ = name; // autofix const artifacts = buildWithTarget(b, cross_target, .ReleaseSafe, .none, version, no_llvm); + _ = artifacts; // autofix - inline for (.{ - .{ artifacts.compiler, "compiler" }, - // .{ artifacts.lsp, "lsp" }, - }) |entry| { - const artifact, const bin_name = entry; - package.dependOn(&b.addInstallFile( - artifact.getEmittedBin(), - b.pathJoin(&.{ - "package", - name, - std.mem.concat(b.allocator, u8, &.{ - bin_name, - cross_target.result.exeFileExt(), - }) catch @panic("OOM"), - }), - ).step); - } + // inline for (.{ + // .{ artifacts.compiler, "compiler" }, + // // .{ artifacts.lsp, "lsp" }, + // }) |entry| { + // const artifact, const bin_name = entry; + // package.dependOn(&b.addInstallFile( + // artifact.getEmittedBin(), + // b.pathJoin(&.{ + // "package", + // name, + // std.mem.concat(b.graph.arena, u8, &.{ + // bin_name, + // cross_target.result.exeFileExt(), + // }) catch @panic("OOM"), + // }), + // ).step); + // } package.dependOn(&b.addInstallFile(b.path("CHANGELOG.md"), "package/CHANGELOG.md").step); package.dependOn(&b.addInstallFile(manual_out, "package/README.html").step); @@ -173,11 +190,48 @@ pub fn build(b: *std.Build) !void { const TracingMode = enum { none, spall, chrome, log }; const Artifacts = struct { + no_llvm: bool, + target: Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + core: *Build.Module, core_tests: *Build.Step.Compile, - compiler: *Build.Step.Compile, - lsp: *Build.Step.Compile, opts_mod: *Build.Module, + behavior_tests: *Build.Step.Compile, + + /// Construct a CLI with certain features. + /// When developing on a small component, such as the compiler core, you + /// don't want to compile every part of the compiler. This returns a CLI + /// compilation configured with only the features that are desired. + fn cli(a: Artifacts, b: *Build, config: BuildConfig) *Build.Step.Compile { + const dep_opts = .{ .target = a.target, .optimize = a.optimize }; + const use_llvm = if (a.no_llvm) false else null; + const use_lld = if (a.no_llvm) false else null; + + const tracer = b.dependency("tracer", dep_opts); + + // Options + const build_config = b.addOptions(); + build_config.addOption(BuildConfig, "config", config); + const build_config_mod = build_config.createModule(); + + const exe = b.addExecutable(.{ + .name = "compiler", + .root_source_file = b.path("src/cli.zig"), + .target = a.target, + .optimize = a.optimize, + .use_lld = use_lld, + .use_llvm = use_llvm, + .strip = false, + .omit_frame_pointer = false, + .single_threaded = true, + }); + exe.root_module.addImport("build_options", a.opts_mod); + exe.root_module.addImport("build_config", build_config_mod); + exe.root_module.addImport("core", a.core); + exe.root_module.addImport("tracer", tracer.module("tracer")); + return exe; + } }; fn buildWithTarget( @@ -226,43 +280,39 @@ fn buildWithTarget( core_tests.root_module.addImport("tracer", tracer.module("tracer")); core_tests.root_module.addImport("zg:PropsData", zg.module("PropsData")); - // Compiler Demo Exe - const compiler_exe = b.addExecutable(.{ - .name = "compiler", - .root_source_file = b.path("src/core/main.zig"), + const behavior_tests = b.addExecutable(.{ + .name = "run_behavior_tests", + .root_source_file = b.path("test/behavior.zig"), .target = target, .optimize = optimize, .use_lld = use_lld, .use_llvm = use_llvm, - .strip = false, - .omit_frame_pointer = false, - .single_threaded = true, }); - compiler_exe.root_module.addImport("build_options", opts_mod); - compiler_exe.root_module.addImport("core", core); - compiler_exe.root_module.addImport("tracer", tracer.module("tracer")); - - // Language Server - const lsp_exe = b.addExecutable(.{ - .name = "lsp", - .root_source_file = b.path("src/lsp/lsp.zig"), - .target = target, - .optimize = optimize, - .use_lld = use_lld, - .use_llvm = use_llvm, - .strip = false, - .omit_frame_pointer = false, - .unwind_tables = false, - }); - lsp_exe.root_module.addImport("build_options", opts_mod); - lsp_exe.root_module.addImport("core", core); - lsp_exe.root_module.addImport("tracer", tracer.module("tracer")); + behavior_tests.root_module.addImport("core", core); + behavior_tests.root_module.addImport("build_options", opts_mod); + behavior_tests.root_module.addImport("tracer", tracer.module("tracer")); + behavior_tests.root_module.addImport("quickjs", b.dependency("quickjs", dep_opts).module("quickjs")); return .{ + .no_llvm = no_llvm, + .target = target, + .optimize = optimize, + .core = core, .core_tests = core_tests, - .compiler = compiler_exe, - .lsp = lsp_exe, .opts_mod = opts_mod, + .behavior_tests = behavior_tests, }; } + +pub const PathOrString = union(enum) { + path: LazyPath, + string: []const u8, +}; + +const std = @import("std"); +const Build = std.Build; +const LazyPath = Build.LazyPath; +const Target = std.Target; +const OsVersion = Target.Query.OsVersion; +const BuildConfig = @import("src/BuildConfig.zig"); diff --git a/build.zig.zon b/build.zig.zon index b94be8dde6b2a04eb4035309b39e497669b683e6..a0db0b7448835ba1c91fb84121bb9e389edcc80e 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -13,10 +13,16 @@ .url = "https://github.com/paperdave/zig-tracer/archive/d4c2eff040e9e69b9f08d2175b315f3055ad7ba3.tar.gz", .hash = "12202d60766a46ceb4074ce5651a58a11f2f6b32927476a55c6889805073e8e3d261", }, + .quickjs = .{ + .path = "src/deps/quickjs", + }, }, + .paths = .{ "build.zig", "build.zig.zon", + "doc", "src", + "test", }, } diff --git a/src/BuildConfig.zig b/src/BuildConfig.zig new file mode 100644 index 0000000000000000000000000000000000000000..bb2c55c001581abbca76dbaebe0043e3bcdd09f7 --- /dev/null +++ b/src/BuildConfig.zig @@ -0,0 +1,9 @@ +have_embedded_quickjs: bool = false, + +pub const minimal: BuildConfig = .{}; + +pub const all: BuildConfig = .{ + .have_embedded_quickjs = true, +}; + +const BuildConfig = @This(); diff --git a/src/cli.zig b/src/cli.zig index ff8747b1c70dce597c04f50c31f357f33458fab7..8ebc9f783f2484dadbeb0901ac340ead4345270a 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -1,6 +1,184 @@ //! This is the `lang` command line interface. It combines all components -//! of the software development kit into one. For development, it may -//! be more reasonable to hack on individual components, which each -//! expose their own CLIs +//! of the software development kit into one. Multiple versions of this +//! file are built with different subsets of features, allowing you to, +//! for example, test the `core` module's compilation features without +//! needing to compile the formatter. -pub fn main() !void {} +const have_debug_features = builtin.mode == .Debug; + +pub fn main() !u8 { + try tracer.init(); + defer tracer.deinit(); + try tracer.init_thread(null); + defer tracer.deinit_thread(); + + // 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(); + + const use_gpa = builtin.mode == .Debug; + var gpa = if (use_gpa) + std.heap.GeneralPurposeAllocator(.{ + .stack_trace_frames = 100, + }){} + else + std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer _ = if (use_gpa) gpa.deinit(); + const alloc = gpa.allocator(); + + // Current working directory is required for CLI resolution. + var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; + const cwd = try std.posix.getcwd(&cwd_buf); + + // Command line argument parsing. + var maybe_file_path: ?[]const u8 = null; + defer if (maybe_file_path) |path| alloc.free(path); + var exe_name: ?[]const u8 = null; + defer if (exe_name) |path| alloc.free(path); + var dump_ir = false; + var dump_state: ?[]const u8 = null; + var has_errors = false; + { + var args = try std.process.argsWithAllocator(alloc); + defer args.deinit(); + + exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli")); + + while (args.next()) |arg| { + if (std.mem.startsWith(u8, arg, "-")) { + if (have_debug_features and std.mem.eql(u8, arg, "--report-trace")) { + core.Report.debug_show_report_trace = true; + continue; + } + + if (std.mem.eql(u8, arg, "--dump-ir")) { + dump_ir = true; + continue; + } + + if (std.mem.startsWith(u8, arg, "--dump-state=")) { + dump_state = arg["--dump-state=".len..]; + continue; + } + + std.debug.print("error: unknown flag '{s}'\n", .{arg}); + has_errors = true; + break; + } + + if (maybe_file_path == null) { + maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg }); + } else { + std.debug.print("error: multiple files specified\n", .{}); + maybe_file_path = null; + break; + } + } + } + + if (maybe_file_path == null) has_errors = true; + if (has_errors) { + std.debug.print("Usage: {s} [flags] \n", .{exe_name.?}); + { + std.debug.print( + // | 60 chars wrap point| + \\ + \\Flags: + \\ --dump-ir Dump intermediate representations + \\ --dump-state= Dump compiler state to JSON + \\ + , .{}); + } + if (builtin.mode == .Debug) { + std.debug.print( + \\ + \\Debug Mode Flags: + \\ --report-trace Print stack traces per error + \\ + , .{}); + } + return 1; + } + const file_path = maybe_file_path.?; + + // Execute compiler + { + var js_backend = core.Js{}; + + var comp: core.Comp = .{ + .gpa = tracer.tracingAllocator(&alloc), + .options = .{ + .dump_ir = dump_ir, + }, + .backend = .{ .js = &js_backend }, + .out = core.OutStream.init(std.io.getStdOut().writer().any()), + }; + try comp.init(); + defer comp.deinit(); + + defer if (dump_state) |path| { + dumpState(&comp, path) catch |err| { + std.debug.print("Failed to dump compilation state: {}", .{err}); + }; + }; + + const main_file = try comp.getFile(file_path); + + var fail_report: Report = undefined; + const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) { + error.TransformFail => { + std.debug.print("Transform failed:\n", .{}); + try fail_report.dumpToStdErr(&comp); + return 1; + }, + error.ParseFail => { + // Already printed. + return 1; + }, + else => return err, + }; + + if (comp.options.dump_ir) { + const stderr = std.io.getStdErr().writer(); + try teg.dumpToWriter(stderr, null); + } + + const entry_point = comp.semaEntryPoint(main_file) catch |err| switch (err) { + error.AnalysisFail => std.process.exit(1), + else => return err, + }; + + const output = std.io.getStdOut().writer(); + var bufw = std.io.bufferedWriter(output); + const writer = bufw.writer(); + try js_backend.link(&comp, entry_point, writer.any()); + try bufw.flush(); + } + + std.process.cleanExit(); + return 0; +} + +const core = @import("core"); +const Comp = core.Comp; +const Report = core.Report; + +const tracer = @import("tracer"); +pub const tracer_impl = @field(tracer, @import("build_options").tracing); + +const std = @import("std"); +const builtin = @import("builtin"); + +fn dumpState(comp: *const Comp, dump_path: []const u8) !void { + const file = try std.fs.cwd().createFile(dump_path, .{}); + defer file.close(); + + var bufw = std.io.bufferedWriter(file.writer()); + var jw = std.json.writeStream(bufw.writer(), .{ + .whitespace = .indent_2, + }); + + try comp.jsonStringify(&jw); + try bufw.writer().writeAll("\n"); + try bufw.flush(); +} diff --git a/src/core/Air.Dump.zig b/src/core/Air.Dump.zig index 61a9a365ec0242e9849f5aede17665d11af4dcc6..1cda7665d78cc67f1373bfc3790272043dca9396 100644 --- a/src/core/Air.Dump.zig +++ b/src/core/Air.Dump.zig @@ -1,6 +1,7 @@ //! This implements a debug formatter for Teg air: Air, ip: *const InternPool, +tty: std.io.tty.Config, pub const State = struct { arena: std.mem.Allocator, @@ -423,7 +424,7 @@ pub fn format( var state = State{ .air = &air, .ip = dump.ip, - .tty = core.detectTTYFromWriter(writer), + .tty = dump.tty, .arena = arena.allocator(), }; try state.dumpInst(writer, .main_block); diff --git a/src/core/Air.zig b/src/core/Air.zig index add87c643192ec31ead80b408ecdcc4544e81c73..11b470b4dc06cadcf87c037f9014f319d66eeed4 100644 --- a/src/core/Air.zig +++ b/src/core/Air.zig @@ -396,8 +396,8 @@ pub fn paramCount(air: Air) u32 { return @intCast(air.insts.len); } -pub fn dumpToWriter(air: Air, ip: *InternPool, writer: anytype) !void { - try writer.print("{}", .{Dump{ .air = air, .ip = ip }}); +pub fn dumpToWriter(air: Air, ip: *InternPool, out: core.OutStream) !void { + try out.print("{}", .{Dump{ .air = air, .ip = ip, .tty = out.tty }}); } pub const Dump = @import("Air.Dump.zig"); @@ -407,11 +407,11 @@ const std = @import("std"); const mem = std.mem; const assert = std.debug.assert; -const InternPool = @import("InternPool.zig"); +const core = @import("mod.zig"); +const InternPool = core.InternPool; const Value = InternPool.Value; const Type = InternPool.Type; -const Teg = @import("Teg.zig"); const dod = @import("array.zig"); const Unused = enum(u32) { zero = 0 }; diff --git a/src/core/Comp.zig b/src/core/Comp.zig index 6b231aeeebd4353d7b81f39fa3d92a7e90a9751e..d059c601e4825bc4ce300db2cc0f2fff7b54dda7 100644 --- a/src/core/Comp.zig +++ b/src/core/Comp.zig @@ -11,10 +11,15 @@ intern_pool: InternPool = .{}, /// TODO: turn this into a HashMap(*File, void) as File owns it's key files: std.StringHashMapUnmanaged(*File) = .{}, +out: core.OutStream, + options: Options = .{}, backend: Backend, +// TODO: remove when it is possible to return `Report`s nicely +temp_flag_had_errors: bool = false, + pub fn init(comp: *Comp) !void { try comp.intern_pool.init(comp.gpa); } @@ -37,7 +42,7 @@ pub fn semaEntryPoint(comp: *Comp, file: *File) !Decl.Index { // TODO, move this elsewhere. we should print all errors and warnings at once if (teg.report.warnings.len > 0) { - try teg.report.dumpToStdErr(comp); + try teg.report.dumpToWriter(comp, comp.out.writer, comp.out.tty); } const ns = try comp.indexTegModule(file, .main_module_inst); diff --git a/src/core/InternPool.zig b/src/core/InternPool.zig index 2dface82fb4b12aaaef9a302d3d7e70d09bc4dd4..c1c89a763f018e4ccf81a29833fd3b6cd0db1486 100644 --- a/src/core/InternPool.zig +++ b/src/core/InternPool.zig @@ -245,6 +245,10 @@ pub const Value = enum(u32) { return ip.valueToKey(value); } + pub fn assertValid(value: Value, ip: *const InternPool) void { + assert(@intFromEnum(value) < ip.items.len); + } + pub fn dump(value: Value, ip: *const InternPool) std.fmt.Formatter(Value.formatValue) { return .{ .data = .{ .val = value, .ip = ip } }; } @@ -375,6 +379,10 @@ pub const Type = enum(u32) { return @enumFromInt(@intFromEnum(ty)); } + pub fn assertValid(ty: Type, ip: *const InternPool) void { + ty.value().assertValid(ip); + } + pub fn tag(ty: Type, ip: *const InternPool) Type.Tag { if (ty == .void) return .simple_type; return @enumFromInt(@intFromEnum(ty.value().valueTag(ip))); @@ -1256,6 +1264,8 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { break :brk Item.encode(.func, extra); }, .function_type => |f| brk: { + for (f.param_types) |ty| ty.assertValid(ip); + const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.TypeFunction, .{ .params_len = @intCast(f.param_types.len), .return_type = f.return_type, @@ -1265,6 +1275,8 @@ pub fn get(ip: *InternPool, gpa: Allocator, key: Key) !Value { break :brk Item.encode(.function_type, extra); }, .struct_type => |s| brk: { + for (s.field_types) |ty| ty.assertValid(ip); + assert(s.field_keys.len == s.field_types.len); const extra, var trail = try ip.extra.appendTrailing(gpa, Tag.StructType, .{ .fields_len = @intCast(s.field_keys.len), diff --git a/src/core/Sema.zig b/src/core/Sema.zig index 75dd0f374d8b16c23e622a60433488b000fc6880..2a6a11b990225cba620a5070c600a96b1d326c08 100644 --- a/src/core/Sema.zig +++ b/src/core/Sema.zig @@ -63,6 +63,7 @@ pub const Error = error{ // TODO: remove this from the primary error set HitNoReturn, + OutStream, OutOfMemory, } || Comp.File.GetTegError; @@ -123,6 +124,9 @@ pub fn init(s: *Sema) !void { const t = tracer.trace(@src()); defer t.end(); + assert(s.ip.items.len > 0); // did not initialize intern pool + _ = s.ip.items.bytes[0]; // segfault if intern pool is undefined memory + s.report = .{ .gpa = s.gpa, .current_file = s.ip.namespacePtr(s.namespace_index).file_scope, @@ -140,7 +144,10 @@ pub fn deinit(s: *Sema) void { defer t.end(); const report = s.report.finalize() catch @panic("OOM"); - report.dumpToStdErr(s.comp) catch @panic("Error"); + if ((report.errors.len + report.warnings.len) > 0) { + s.comp.temp_flag_had_errors = true; + } + report.dumpToWriter(s.comp, s.comp.out.writer, s.comp.out.tty) catch @panic("Error"); report.deinit(s.gpa); defer s.* = undefined; @@ -158,9 +165,9 @@ pub fn finalize(s: *Sema) !void { if (s.decl.value().valueTag(s.ip) == .func) { if (s.comp.options.dump_ir) { - std.debug.print("=" ** 20, .{}); - std.debug.print("\nAir for {}\n", .{s.decl.value().dump(s.ip)}); - air.dumpToWriter(s.ip, std.io.getStdErr().writer()) catch {}; + try s.comp.out.print("=" ** 20, .{}); + try s.comp.out.print("\nAir for {}\n", .{s.decl.value().dump(s.ip)}); + try air.dumpToWriter(s.ip, s.comp.out); } try s.comp.codeGenDecl(s.decl.value(), &air); @@ -215,7 +222,7 @@ pub fn analyzeFunctionDeclaration(s: *Sema) Error!Value { s.params = try s.gpa.alloc(Type, fn_decl.params.len); defer s.gpa.free(s.params); - for (fn_decl.params.slice(&s.code), 0..) |param, i| { + for (fn_decl.params.slice(&s.code), s.params, 0..) |param, *dest, i| { const src = s.inst_map.start.op(operandArgument(@intCast(i))); const ty = try s.resolveType(src, param); if (!ty.hasPossibleRepresentation(s.ip)) { @@ -224,7 +231,7 @@ pub fn analyzeFunctionDeclaration(s: *Sema) Error!Value { .type = ty, }); } - s.params[i] = ty; + dest.* = ty; } var return_type, const early_value = if (fn_decl.return_type.unwrap()) |ty| brk: { @@ -1462,6 +1469,7 @@ fn binTypeResolution(s: *Sema, src: Teg.Inst.Index, op: BinaryOp, lhs: Type, rhs .float => switch (rhs) { .int32 => return .float, .numeric_literal => return .float, + .float => return .float, else => {}, }, else => {}, diff --git a/src/core/StoredTrace.zig b/src/core/StoredTrace.zig index c8ca441a35b2a087ba9ebae134cfec15c49c96bc..81e25a3c092915b4921540b4fe20efbb54bedd2b 100644 --- a/src/core/StoredTrace.zig +++ b/src/core/StoredTrace.zig @@ -5,7 +5,7 @@ data: [31]usize, index: usize, -pub const empty = .{ +pub const empty: StoredTrace = .{ .data = .{0} ** 31, .index = 0, }; @@ -29,7 +29,7 @@ pub fn from(stack_trace: ?*std.builtin.StackTrace) StoredTrace { if (stack_trace) |stack| { var data: [31]usize = undefined; @memset(&data, 0); - const items = @min(stack.instruction_addresses.len, 32); + const items = @min(stack.instruction_addresses.len, 31); @memcpy(data[0..items], stack.instruction_addresses[0..items]); return .{ .data = data, diff --git a/src/core/Teg.Dump.zig b/src/core/Teg.Dump.zig index 91c3f7556fedf350c5ef076b22e53674bf8f1c95..50b13903681b57d7647141a0d22724c09b9dc261 100644 --- a/src/core/Teg.Dump.zig +++ b/src/core/Teg.Dump.zig @@ -1,5 +1,6 @@ //! This implements a debug formatter for Teg teg: Teg, +tty: ?std.io.tty.Config = null, const State = struct { arena: std.mem.Allocator, @@ -410,7 +411,7 @@ pub fn format( const teg = dump.teg; var state = State{ .teg = &teg, - .tty = detectTTYFromWriter(writer), + .tty = dump.tty orelse detectTTYFromWriter(writer), .arena = arena.allocator(), }; try state.dumpInst(writer, .main_module_inst); diff --git a/src/core/Teg.zig b/src/core/Teg.zig index 5916b860690a9213b6fac0fbdb8157e01d458ef4..b1ed7ebd819f19aac74226aa82ff3126ef67e77b 100644 --- a/src/core/Teg.zig +++ b/src/core/Teg.zig @@ -619,8 +619,8 @@ pub fn declName(teg: *const Teg, decl_index: Inst.Index) []const u8 { return decl_data.name.slice(teg.string_bytes); } -pub fn dumpToWriter(teg: Teg, writer: anytype) !void { - try writer.print("{}", .{Dump{ .teg = teg }}); +pub fn dumpToWriter(teg: Teg, writer: anytype, tty: ?std.io.tty.Config) !void { + try writer.print("{}", .{Dump{ .teg = teg, .tty = tty }}); } const Teg = @This(); diff --git a/src/core/js/NodeGen.zig b/src/core/js/NodeGen.zig index 88f0e2bdb3f920ac45fd0f92fd697039bace90ef..8165ef0cb034a08a275ba0104dc650994604f259 100644 --- a/src/core/js/NodeGen.zig +++ b/src/core/js/NodeGen.zig @@ -478,7 +478,13 @@ fn fetchInstExpression(c: *NodeGen, index: Air.Inst.Index) !Node.Index { }); } else unreachable, .single => {}, - .multi => assert(c.nodes.items(.tag)[@intFromEnum(data.node)] == .local), + .multi => { + // multi-references can only duplicate identifier nodes. + assert(switch (c.nodes.items(.tag)[@intFromEnum(data.node)]) { + .local, .global => true, + else => false, + }); + }, } } return data.node; diff --git a/src/core/main.zig b/src/core/main.zig deleted file mode 100644 index 10a0f30c615fe7f9a5da1dced31f44b0d5513a7b..0000000000000000000000000000000000000000 --- a/src/core/main.zig +++ /dev/null @@ -1,170 +0,0 @@ -//! This file is used to test the compiler implementation without -//! needing to build the language server and other tooling. -const std = @import("std"); -const builtin = @import("builtin"); -const core = @import("core"); - -const Tokenizer = core.Tokenizer; -const Parse = core.Parse; -const Transform = core.Transform; -const Comp = core.Comp; -const unicode_props = core.unicode_props; -pub const Report = core.Report; - -const tracer = @import("tracer"); -pub const tracer_impl = @field(tracer, @import("build_options").tracing); - -pub fn main() !void { - try tracer.init(); - defer tracer.deinit(); - try tracer.init_thread(null); - defer tracer.deinit_thread(); - - try unicode_props.init(); - - const use_gpa = builtin.mode == .Debug; - var gpa = if (use_gpa) - std.heap.GeneralPurposeAllocator(.{ - .stack_trace_frames = 100, - }){} - else - std.heap.ArenaAllocator.init(std.heap.page_allocator); - defer _ = if (use_gpa) gpa.deinit(); - const alloc = gpa.allocator(); - - var maybe_file_path: ?[]const u8 = null; - defer if (maybe_file_path) |path| alloc.free(path); - - var cwd_buf: [std.fs.max_path_bytes]u8 = undefined; - const cwd = try std.posix.getcwd(&cwd_buf); - - var exe_name: []const u8 = undefined; - var dump_ir = false; - var dump_state: ?[]const u8 = null; - - { - var args = try std.process.argsWithAllocator(alloc); - defer args.deinit(); - - exe_name = try alloc.dupe(u8, std.fs.path.basename(args.next() orelse "cli")); - errdefer alloc.free(exe_name); - - while (args.next()) |arg| { - if (std.mem.startsWith(u8, arg, "-")) { - if (builtin.mode == .Debug and std.mem.eql(u8, arg, "--report-trace")) { - Report.debug_show_report_trace = true; - continue; - } - if (std.mem.eql(u8, arg, "--dump-ir")) { - dump_ir = true; - continue; - } - if (std.mem.startsWith(u8, arg, "--dump-state=")) { - dump_state = arg["--dump-state=".len..]; - continue; - } - std.debug.print("error: unknown flag '{s}'\n", .{arg}); - maybe_file_path = null; - break; - } - - if (maybe_file_path == null) { - maybe_file_path = try std.fs.path.resolve(alloc, &.{ cwd, arg }); - } else { - std.debug.print("error: multiple files specified\n", .{}); - maybe_file_path = null; - break; - } - } - } - defer alloc.free(exe_name); - - const file_path = maybe_file_path orelse { - std.debug.print("Usage: {s} [flags] \n", .{exe_name}); - { - std.debug.print( - // | 60 chars wrap point| - \\ - \\Flags: - \\ --dump-ir Dump intermediate representations - \\ --dump-state= Dump compiler state to JSON - \\ - , .{}); - } - if (builtin.mode == .Debug) { - std.debug.print( - \\ - \\Debug Mode Flags: - \\ --report-trace Print stack traces per error - \\ - , .{}); - } - std.process.exit(1); - }; - - var js_backend = core.Js{}; - - var comp: Comp = .{ - .gpa = tracer.tracingAllocator(&alloc), - .options = .{ - .dump_ir = dump_ir, - }, - .backend = .{ .js = &js_backend }, - }; - try comp.init(); - defer comp.deinit(); - - defer if (dump_state) |path| { - dumpState(&comp, path) catch |err| { - std.debug.print("Failed to dump compilation state: {}", .{err}); - }; - }; - - const main_file = try comp.getFile(file_path); - - var fail_report: Report = undefined; - const teg = main_file.getTeg(&comp, &fail_report) catch |err| switch (err) { - error.TransformFail => { - std.debug.print("Transform failed:\n", .{}); - try fail_report.dumpToStdErr(&comp); - std.process.exit(1); - }, - error.ParseFail => { - // Already printed. - std.process.exit(1); - }, - else => return err, - }; - - if (comp.options.dump_ir) { - const stderr = std.io.getStdErr().writer(); - try teg.dumpToWriter(stderr); - } - - const entry_point = comp.semaEntryPoint(main_file) catch |err| switch (err) { - error.AnalysisFail => std.process.exit(1), - else => return err, - }; - - const output = std.io.getStdOut().writer(); - var bufw = std.io.bufferedWriter(output); - const writer = bufw.writer(); - try js_backend.link(&comp, entry_point, writer.any()); - try bufw.flush(); - - std.process.cleanExit(); -} - -fn dumpState(comp: *const Comp, dump_path: []const u8) !void { - const file = try std.fs.cwd().createFile(dump_path, .{}); - defer file.close(); - - var bufw = std.io.bufferedWriter(file.writer()); - var jw = std.json.writeStream(bufw.writer(), .{ - .whitespace = .indent_2, - }); - - try comp.jsonStringify(&jw); - try bufw.writer().writeAll("\n"); - try bufw.flush(); -} diff --git a/src/core/mod.zig b/src/core/mod.zig index cf33cf9bef67090e3daf1e01ae16c2edf203af33..090bbb1957a6f8a335d256ee0e87106d961bfe95 100644 --- a/src/core/mod.zig +++ b/src/core/mod.zig @@ -98,6 +98,7 @@ pub fn detectTTYFromWriter(writer: std.io.AnyWriter) std.io.tty.Config { const file: *const std.fs.File = @alignCast(@ptrCast(writer.context)); return std.io.tty.detectConfig(file.*); } + return .no_color; } @@ -114,6 +115,40 @@ pub fn jsonWriterCast(json_writer: anytype) *std.json.WriteStream( return json_writer; } +/// A writer with TTY information. This abstraction is used so that test runners and wasm +/// builds can specify an output writer that supports TTY, even if the underlying writer +/// is an ArrayList writer (detectTTYFromWriter would report .no_color) +pub const OutStream = struct { + writer: std.io.AnyWriter, + tty: std.io.tty.Config, + + /// To make it possible to have reasonable error sets, all logging + /// IO-related errors will be categorized as error.OutStream. We preserve + /// OutOfMemory so that it is still detectable. + pub const Error = error{ OutStream, OutOfMemory }; + + pub fn init(writer: std.io.AnyWriter) OutStream { + return .{ + .writer = writer, + .tty = detectTTYFromWriter(writer), + }; + } + + pub fn print(out: OutStream, comptime fmt: []const u8, args: anytype) Error!void { + out.writer.print(fmt, args) catch |e| switch (e) { + error.OutOfMemory => return Error.OutOfMemory, + else => return Error.OutStream, + }; + } + + pub fn setColor(out: OutStream, color: std.io.tty.Color) !void { + out.tty.setColor(out.writer, color) catch |e| switch (e) { + error.OutOfMemory => return Error.OutOfMemory, + else => return Error.OutStream, + }; + } +}; + const std = @import("std"); const assert = std.debug.assert; diff --git a/src/deps/quickjs/build.zig b/src/deps/quickjs/build.zig new file mode 100644 index 0000000000000000000000000000000000000000..15a610abde29a5ecd1ed49ab1ad3c51d53482cca --- /dev/null +++ b/src/deps/quickjs/build.zig @@ -0,0 +1,128 @@ +pub fn build(b: *Build) !void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const src = b.dependency("quickjs", .{}); + + const version = std.mem.trim( + u8, + try src.builder.build_root.handle.readFileAlloc(b.graph.arena, "VERSION", 4096), + " \t\r\n", + ); + + const c_flags: []const []const u8 = &.{ + "-Wall", + "-Wextra", + "-Wno-sign-compare", + "-Wno-missing-field-initializers", + "-Wundef", + "-Wuninitialized", + "-Wunused", + "-Wno-unused-parameter", + "-Wwrite-strings", + "-Wchar-subscripts", + "-funsigned-char", + "-fwrapv", + }; + + const defines: []const struct { name: []const u8, value: []const u8 } = &.{ + .{ .name = "_GNU_SOURCE", .value = "1" }, + .{ .name = "CONFIG_BIGNUM", .value = "1" }, + .{ .name = "CONFIG_VERSION", .value = b.fmt("\"{s}\"", .{version}) }, + }; + + const libquickjs = b.addStaticLibrary(.{ + .name = "quickjs", + .target = target, + .optimize = optimize, + }); + + libquickjs.addCSourceFiles(.{ + .root = src.path(""), + .files = &.{ + "quickjs.c", + "libregexp.c", + "libbf.c", + "libunicode.c", + "cutils.c", + }, + .flags = c_flags, + }); + for (defines) |def| { + libquickjs.root_module.addCMacro(def.name, def.value); + } + + const qjsc = b.addExecutable(.{ + .name = "qjsc", + .target = b.graph.host, + .optimize = .Debug, + }); + qjsc.addCSourceFiles(.{ + .root = src.path(""), + .files = &.{ + "qjsc.c", + "quickjs-libc.c", + "quickjs.c", + "libregexp.c", + "libbf.c", + "libunicode.c", + "cutils.c", + }, + .flags = c_flags, + }); + for (defines) |def| { + qjsc.root_module.addCMacro(def.name, def.value); + } + + const qjs = b.addExecutable(.{ + .name = "qjs", + .target = target, + .optimize = optimize, + }); + qjs.addCSourceFiles(.{ + .root = src.path(""), + .files = &.{ + "qjs.c", + "quickjs-libc.c", + }, + .flags = c_flags, + }); + qjs.root_module.addCSourceFile(.{ + .file = runQjsc(b, qjsc, src.path("repl.js")), + .flags = c_flags, + }); + qjs.root_module.addCSourceFile(.{ + .file = runQjsc(b, qjsc, src.path("qjscalc.js")), + .flags = c_flags, + }); + for (defines) |def| { + qjs.root_module.addCMacro(def.name, def.value); + } + qjs.linkLibrary(libquickjs); + + b.installArtifact(qjs); + b.installArtifact(libquickjs); + + const translate = b.addTranslateC(.{ + .root_source_file = src.path("quickjs.h"), + .target = target, + .optimize = optimize, + .link_libc = false, + }); + const module = translate.addModule("quickjs"); + module.linkLibrary(libquickjs); +} + +fn runQjsc(b: *Build, qjsc: *Build.Step.Compile, in: Build.LazyPath) Build.LazyPath { + const run = b.addRunArtifact(qjsc); + run.addArg("-c"); + run.addArg("-fbignum"); + run.addArg("-o"); + const out = run.addOutputFileArg(b.fmt("{s}.c", .{in.getDisplayName()})); + run.addArg("-m"); + run.addFileArg(in); + return out; +} + +const std = @import("std"); +const Build = std.Build; diff --git a/src/deps/quickjs/build.zig.zon b/src/deps/quickjs/build.zig.zon new file mode 100644 index 0000000000000000000000000000000000000000..07543cf53f76810f26a6d3cc985de0218484a348 --- /dev/null +++ b/src/deps/quickjs/build.zig.zon @@ -0,0 +1,16 @@ +.{ + .name = "quickjs", + .version = "0.0.0", + + .dependencies = .{ + .quickjs = .{ + .url = "https://github.com/bellard/quickjs/archive/6e2e68fd0896957f92eb6c242a2e048c1ef3cae0.tar.gz", + .hash = "1220d49e57fc131eebbf4b09fb100f5d802b909d2f36e15d2d849a72f354cd24489d", + }, + }, + + .paths = .{ + "build.zig", + "build.zig.zon", + }, +} diff --git a/src/lsp/lsp.zig b/src/lsp/lsp.zig deleted file mode 100644 index 340c01d5d3878669148836d67c7515697cf04ccb..0000000000000000000000000000000000000000 --- a/src/lsp/lsp.zig +++ /dev/null @@ -1,41 +0,0 @@ -const std = @import("std"); -const builtin = @import("builtin"); -const core = @import("core"); -const tracer = @import("tracer"); - -pub const tracer_impl = @field(tracer, @import("build_options").tracing); - -pub fn main() !void { - if (builtin.mode == .Debug) { - // zig build system doesnt print a newline :/ - std.debug.print("\r\x1b[K", .{}); - } - - std.debug.print("All your codebase are belong to us\n", .{}); - - const use_gpa = builtin.mode == .Debug; - var gpa_state: if (use_gpa) std.heap.GeneralPurposeAllocator(.{ - .stack_trace_frames = 100, - }) else struct {} = .{}; - defer _ = if (use_gpa) gpa_state.deinit(); - const gpa = if (use_gpa) - gpa_state.allocator() - else - std.heap.c_allocator; - - // if `parse` returns `error.ParseFail`, this `err` is set. - // - // at some point later this will change to be a `Report` - // so that multiple parse errors may be reported at once. - var err: core.Parse.AstError.WithTrace = undefined; - var ast = try core.Parse.parse(gpa, - \\let x = 1; - \\ - \\pub fn hello() -> float { - \\ 42.2 - \\} - , &err); - defer ast.deinit(gpa); - - try ast.dumpToWriter(std.io.getStdErr().writer()); -} diff --git a/src/manual/generate-man.zig b/src/manual/generate-man.zig index 7e6654136a4a947e5a4ba349e5f72cd988c86529..a35a68db5bd774fbd011bcfe91692ec4eebd7bd3 100644 --- a/src/manual/generate-man.zig +++ b/src/manual/generate-man.zig @@ -1,46 +1,73 @@ //! Language reference compiler. Converts markdown into HTML, using //! the Tokenizer to highlight code, and the compiler and node.js to //! show code results. Only ever built in Debug mode. We are not -//! smart about memory. Written at 3am on a Thursday. Enjoy +//! smart about memory. Initially written at 3am on a Thursday. Enjoy pub const std = @import("std"); pub const core = @import("core"); +const mem = std.mem; +const Allocator = mem.Allocator; const ArrayList = std.ArrayList; +const ArrayListUnmanaged = std.ArrayListUnmanaged; +const assert = std.debug.assert; + +comptime { + assert(std.debug.runtime_safety); +} pub fn main() !void { comptime std.debug.assert(@import("builtin").mode == .Debug); - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); - const gpa = arena.allocator(); + var gpa_state: std.heap.GeneralPurposeAllocator(.{}) = .{}; + const gpa = gpa_state.allocator(); + + const progress = std.Progress.start(.{ + .root_name = "Build manual", + .estimated_total_items = 0, + }); var args = try std.process.argsWithAllocator(gpa); _ = args.next().?; const compiler_path = args.next().?; + const runtime_path = args.next().?; const markdown_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); const template_src = try std.fs.cwd().readFileAlloc(gpa, args.next().?, std.math.maxInt(usize)); const out_path = args.next().?; - var output = std.ArrayList(u8).init(gpa); - defer output.deinit(); + assert(args.next() == null); + + var contents = ArrayList(u8).init(gpa); + var header: HeaderWriter = .{}; + try processMarkdown( gpa, + progress, markdown_src, compiler_path, - output.writer(), + runtime_path, + &header, + contents.writer(), ); - const applied_template = try std.mem.replaceOwned( - u8, - gpa, - template_src, - "{contents}", - output.items, - ); - defer gpa.free(applied_template); + try header.end(gpa); + + const applied_template = try replaceInTemplate(gpa, template_src, .{ + .contents = contents.items, + .navigation = header.data.items, + .nav_h1 = header.h1.?, + .repository = "https://github.com/paperdave/wl", + }); const out_file = try std.fs.createFileAbsolute(out_path, .{}); defer out_file.close(); try out_file.writeAll(applied_template); + + progress.end(); + + std.debug.lockStdErr(); + std.debug.unlockStdErr(); + + std.process.exit(0); } const BlockType = enum { @@ -48,6 +75,9 @@ const BlockType = enum { h1, h2, h3, + h4, + h5, + h6, code_run, code_build, code, @@ -60,6 +90,7 @@ const BlockType = enum { const InlineType = enum { bold, code, + link, }; const MarkdownTokenizer = struct { @@ -67,7 +98,7 @@ const MarkdownTokenizer = struct { str: []const u8, fn skipWhitespace(t: *MarkdownTokenizer) void { - t.str = std.mem.trimLeft(u8, t.str, " \r\n"); + t.str = mem.trimLeft(u8, t.str, " \r\n"); } fn advance(t: *MarkdownTokenizer, n: usize) []const u8 { @@ -78,11 +109,11 @@ const MarkdownTokenizer = struct { } fn advanceLine(t: *MarkdownTokenizer) []const u8 { - return std.mem.trim(u8, t.advanceLineRaw(), " \r\n"); + return mem.trim(u8, t.advanceLineRaw(), " \r\n"); } fn advanceLineRaw(t: *MarkdownTokenizer) []const u8 { - const n = std.mem.indexOfScalar(u8, t.str, '\n'); + const n = mem.indexOfScalar(u8, t.str, '\n'); const line = t.advance(if (n) |x| x + 1 else t.str.len); return line; } @@ -98,6 +129,9 @@ const MarkdownTokenizer = struct { if (t.str[1] != '#') return .h1; if (t.str[2] != '#') return .h2; if (t.str[3] != '#') return .h3; + if (t.str[4] != '#') return .h4; + if (t.str[5] != '#') return .h5; + if (t.str[6] != '#') return .h6; @panic("headers nested too deep"); }, '>' => { @@ -108,13 +142,13 @@ const MarkdownTokenizer = struct { return .table; }, '`' => { - if (std.mem.startsWith(u8, t.str, "```")) { + if (mem.startsWith(u8, t.str, "```")) { defer _ = t.advanceLine(); - if (std.mem.startsWith(u8, t.str, "```lang:run")) + if (mem.startsWith(u8, t.str, "```lang:run")) return .code_run; - if (std.mem.startsWith(u8, t.str, "```lang:build")) + if (mem.startsWith(u8, t.str, "```lang:build")) return .code_build; - if (std.mem.startsWith(u8, t.str, "```lang")) + if (mem.startsWith(u8, t.str, "```lang")) return .code; return .code_generic; } @@ -130,7 +164,53 @@ const MarkdownTokenizer = struct { } }; -fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []const u8, writer: anytype) !void { +const HeaderWriter = struct { + data: std.ArrayListUnmanaged(u8) = .{}, + level: u8 = 0, + h1: ?[]const u8 = null, + + pub fn add(header: *HeaderWriter, alloc: Allocator, name: []const u8, id: []const u8, level: u8) !void { + const w = header.data.writer(alloc); + + while (header.level > level) { + header.level -= 1; + try w.writeAll(""); + } + + while (header.level < level) { + if (header.level == 0) { + try w.writeAll("
    "); + } else { + try w.writeAll("
    • "); + } + header.level += 1; + } + + try w.print("
    • {s}
    • ", .{ id, name }); + } + + pub fn end(header: *HeaderWriter, alloc: Allocator) !void { + const w = header.data.writer(alloc); + while (header.level > 1) { + header.level -= 1; + try w.writeAll("
  • "); + } + if (header.level == 1) { + header.level -= 1; + try w.writeAll("
"); + } + } +}; + +fn processMarkdown( + gpa: Allocator, + progress: std.Progress.Node, + src: []const u8, + compiler_path: []const u8, + runtime_path: []const u8, + header: *HeaderWriter, + writer: anytype, +) !void { var t = MarkdownTokenizer{ .str = src }; t.skipWhitespace(); @@ -157,13 +237,44 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con .h1, .h2, .h3, + .h4, + .h5, + .h6, => |tag| { const html_tag = @tagName(tag); const name = t.advanceLine(); - try writer.print("<{0s}>{1s}\n", .{ - html_tag, - name, - }); + const id = try nameToHeaderId(gpa, name); + + if (tag != .h1) { + try writer.print("<{0s} id=\"{2s}\">{1s}\n", .{ + html_tag, + name, + id, + }); + + if (tag != .h1) + try header.add(gpa, name, id, switch (tag) { + .h2 => 1, + .h3 => 2, + .h4 => 3, + .h5 => 4, + .h6 => 5, + else => unreachable, + }); + } else { + assert(header.h1 == null); + + header.h1 = try std.fmt.allocPrint( + gpa, + "{s}", + .{name}, + ); + + try writer.print("<{0s}>{1s}\n", .{ + html_tag, + name, + }); + } }, .code_run, .code_build, @@ -173,50 +284,154 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con var code = ArrayList(u8).init(gpa); var next_line = t.advanceLineRaw(); - while (t.str.len > 0 and !std.mem.startsWith(u8, next_line, "```")) { + while (t.str.len > 0 and !mem.startsWith(u8, next_line, "```")) { try code.appendSlice(next_line); next_line = t.advanceLineRaw(); } + if (tag == .code_run or tag == .code_build or tag == .code) { + try writer.writeAll("
hello.wl
"); + } else { + try writer.writeAll("
"); + } + try writer.writeAll("
");
-                try writeTokenizedCode(gpa, code.items, writer);
+                if (tag == .code_generic) {
+                    try writeHtmlEscapedSlice(code.items, writer, false);
+                } else {
+                    try writeTokenizedCode(gpa, code.items, writer);
+                }
                 try writer.writeAll("
"); try code.append('\n'); - switch (tag) { - .code_run, - .code_build, - => { - var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa); - build.stdin_behavior = .Pipe; - build.stdout_behavior = .Pipe; - build.stderr_behavior = .Pipe; - var env_map = try std.process.getEnvMap(gpa); - defer env_map.deinit(); - try env_map.put("YES_COLOR", "1"); - build.env_map = &env_map; - try build.spawn(); - try build.stdin.?.writeAll(code.items); - var out = ArrayList(u8).init(gpa); - var err = ArrayList(u8).init(gpa); - try build.collectOutput(&out, &err, std.math.maxInt(usize)); - const result = try build.wait(); - _ = result; - if (std.mem.trim(u8, err.items, " \n\r").len > 0) { - try writer.writeAll("
");
-                            try writeAnsi(gpa, err.items, writer);
-                            try writer.writeAll("
"); + if (tag == .code_run or tag == .code_build) { + var prog = progress.start("Code Snippet", if (tag == .code_run) 2 else 1); + defer prog.end(); + + var prog_step = prog.start("Compile", 1); + var build = std.process.Child.init(&.{ compiler_path, "/dev/stdin" }, gpa); + build.stdin_behavior = .Pipe; + build.stdout_behavior = .Pipe; + build.stderr_behavior = .Pipe; + var env_map = try std.process.getEnvMap(gpa); + defer env_map.deinit(); + try env_map.put("YES_COLOR", "1"); + build.env_map = &env_map; + try build.spawn(); + try build.stdin.?.writeAll(code.items); + var out = ArrayList(u8).init(gpa); + var err = ArrayList(u8).init(gpa); + try build.collectOutput(&out, &err, std.math.maxInt(usize)); + const result = try build.wait(); + if (result != .Exited or result.Exited != 0) { + try err.writer().print("[Process {s} with {d}]", .{ + switch (result) { + .Exited => "exited", + .Stopped => "stopped", + .Signal => "signaled", + .Unknown => "hit unknown state", + }, + switch (result) { + inline else => |d| @as(u32, d), + }, + }); + } + if (mem.trim(u8, err.items, " \n\r").len > 0) { + try writer.writeAll("
");
+                        try writeAnsi(gpa, err.items, writer);
+                        try writer.writeAll("
"); + } + const trimmed_out = mem.trim(u8, out.items, " \n\r"); + if (trimmed_out.len > 0) { + try writer.writeAll("
");
+                        try writer.writeAll("// Compiled JavaScript Code");
+                        try writer.writeAll(trimmed_out[mem.indexOfScalar(u8, trimmed_out, '\n').?..]);
+                        try writer.writeAll("
"); + } + + prog.completeOne(); + prog_step.end(); + + if (tag == .code_run and runtime_path.len > 0) { + const run_step = prog.start("Execute", 0); + defer run_step.end(); + + var temp_dir = std.testing.tmpDir(.{}); + defer temp_dir.cleanup(); + + const input_sub_path = "result.js"; + try temp_dir.dir.writeFile(.{ + .sub_path = input_sub_path, + .data = trimmed_out, + }); + + var env_map_run = try std.process.getEnvMap(gpa); + defer env_map_run.deinit(); + try env_map_run.put("NO_COLOR", "1"); + try env_map_run.put("TERM", "dumb"); + + var run_out = ArrayList(u8).init(gpa); + var run_err = ArrayList(u8).init(gpa); + + // Different JavaScript runtimes have different quirks with + // regards to CLI parsing and standard input streams. + const run_sub_command = std.mem.indexOf(u8, runtime_path, "deno") != null or + std.mem.indexOf(u8, runtime_path, "porf") != null; + + const cmd: []const []const u8 = if (run_sub_command) + &.{ runtime_path, "run", input_sub_path } + else + &.{ runtime_path, input_sub_path }; + + var run = std.process.Child.init(cmd, gpa); + + run.cwd_dir = temp_dir.dir; + var path_buf: [std.fs.max_path_bytes]u8 = undefined; + if (@import("builtin").os.tag == .windows) + run.cwd = try std.os.getFdPath(temp_dir.dir.fd, &path_buf); + + run.stdin_behavior = .Ignore; + run.stdout_behavior = .Pipe; + run.stderr_behavior = .Pipe; + + run.env_map = &env_map_run; + try run.spawn(); + + try run.collectOutput(&run_out, &run_err, std.math.maxInt(usize)); + const run_result = run.wait() catch |e| { + if (e == error.FileNotFound) { + std.debug.panic( + \\Missing JavaScript evaluator: {s} + \\Specify another one using -Djs_runtime=... or set to "" to skip evaluation. + \\ + , .{runtime_path}); + } + return e; + }; + + if (run_result != .Exited or run_result.Exited != 0) { + try run_err.writer().print("[Process {s} with {d}]", .{ + switch (run_result) { + .Exited => "exited", + .Stopped => "stopped", + .Signal => "signaled", + .Unknown => "hit unknown state", + }, + switch (run_result) { + inline else => |d| @as(u32, d), + }, + }); } - if (std.mem.trim(u8, out.items, " \n\r").len > 0) { - try writer.writeAll("
");
-                            try writer.writeAll("// Compiled JavaScript Code");
-                            try writer.writeAll(out.items[std.mem.indexOfScalar(u8, out.items, '\n').?..]);
-                            try writer.writeAll("
"); - } - }, - else => {}, + + try writer.writeAll("
");
+                        try writer.writeAll(run_out.items);
+                        try writer.writeAll(run_err.items);
+                        try writer.writeAll("
"); + } } + + try writer.writeAll("
"); }, .ul, => { @@ -235,7 +450,7 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con try writer.writeAll(""); { try writer.writeAll(""); - var it = std.mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); + var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); while (it.next()) |tok| { try writer.writeAll(""); - var it = std.mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); + var it = mem.tokenizeScalar(u8, t.advanceLine()[1..], '|'); while (it.next()) |tok| { try writer.writeAll("
"); try processInlineMarkdown(gpa, tok, writer); @@ -247,7 +462,7 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con t.skipWhitespace(); while (t.str.len > 0 and t.str[0] == '|') { try writer.writeAll("
"); try processInlineMarkdown(gpa, tok, writer); @@ -282,7 +497,7 @@ fn processMarkdown(gpa: std.mem.Allocator, src: []const u8, compiler_path: []con pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { var pos: usize = 0; while (true) : (pos += 1) { - pos = std.mem.indexOfAnyPos(u8, src, pos, "*`") orelse return null; + pos = mem.indexOfAnyPos(u8, src, pos, "*`[") orelse return null; switch (src[pos]) { '*' => switch (src[pos + 1]) { @@ -290,12 +505,13 @@ pub fn nextInlineToken(src: []const u8) ?struct { InlineType, usize } { else => continue, }, '`' => return .{ .code, pos }, + '[' => return .{ .link, pos }, else => unreachable, } } } -fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytype) !void { +fn processInlineMarkdown(gpa: Allocator, src: []const u8, writer: anytype) !void { var bold = false; var remain = src; @@ -312,13 +528,32 @@ fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytyp remain = remain[2..]; }, .code => { - const end = std.mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); + const end = mem.indexOfScalarPos(u8, remain, 1, '`') orelse @panic("invalid code block"); const code = remain[1..end]; try writer.writeAll(""); try writeTokenizedCode(gpa, code, writer); try writer.writeAll(""); remain = remain[end + 1 ..]; }, + .link => { + const label_end = mem.indexOfScalarPos(u8, remain, 1, ']') orelse @panic("invalid link block"); + assert(remain[label_end + 1] == '('); // invalid link. '[' syntax is not implemented + + const href_end = mem.indexOfScalarPos(u8, remain, label_end + 2, ')') orelse @panic("invalid link block"); + + const label = remain[1..label_end]; + const href = remain[label_end + 2 .. href_end]; + + try writer.writeAll(""); + try processInlineMarkdown(gpa, label, writer); + try writer.writeAll(""); + remain = remain[href_end + 1 ..]; + }, } } try writer.writeAll(remain); @@ -330,7 +565,7 @@ fn processInlineMarkdown(gpa: std.mem.Allocator, src: []const u8, writer: anytyp // TODO: Change this to use Tree-sitter, which would also make it very simple // to highlight JavaScript code in addition to the language. -fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void { +fn writeTokenizedCode(gpa: Allocator, code: []const u8, writer: anytype) !void { const code_z = try gpa.dupeZ(u8, code); defer gpa.free(code_z); @@ -344,7 +579,7 @@ fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) t.pos += 1; continue; } orelse break; - try writer.writeAll(t.code[last..tok.offset]); + try writeHtmlEscapedSlice(t.code[last..tok.offset], writer, false); try writer.print("{s}", .{ @tagName(tok.tag), if (tok.tag.isKeyword()) " kw" else "", @@ -352,17 +587,18 @@ fn writeTokenizedCode(gpa: std.mem.Allocator, code: []const u8, writer: anytype) }); last = t.pos; } + try writeHtmlEscapedSlice(t.code[last..], writer, false); } -fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void { +fn writeAnsi(gpa: Allocator, code: []const u8, writer: anytype) !void { var remain = code; var count: usize = 0; - while (std.mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| { - try writer.writeAll(remain[0..esc_i]); + while (mem.indexOfScalar(u8, remain, '\x1b')) |esc_i| { + try writeHtmlEscapedSlice(remain[0..esc_i], writer, false); switch (remain[esc_i + 1]) { '[' => switch (remain[esc_i + 2]) { '0'...'9' => { - const end = std.mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?; + const end = mem.indexOfScalarPos(u8, remain, esc_i + 2, 'm').?; const int = std.fmt.parseInt(u8, remain[esc_i + 2 .. end], 10) catch unreachable; switch (int) { else => { @@ -385,3 +621,88 @@ fn writeAnsi(gpa: std.mem.Allocator, code: []const u8, writer: anytype) !void { } _ = gpa; } + +fn nameToHeaderId(alloc: Allocator, input: []const u8) ![]const u8 { + var out = try ArrayList(u8).initCapacity(alloc, input.len); + + var is_split = false; + for (input) |char| { + switch (char) { + 'a'...'z', 'A'...'Z', '0'...'9' => { + if (is_split) { + out.appendAssumeCapacity('-'); + is_split = false; + } + out.appendAssumeCapacity(std.ascii.toLower(char)); + }, + else => { + is_split = true; + }, + } + } + + return try out.toOwnedSlice(); +} + +fn replaceInTemplate(alloc: Allocator, template: []const u8, replacements: anytype) ![]const u8 { + const Replacements = @TypeOf(replacements); + const Map = std.StringArrayHashMapUnmanaged([]const u8); + + var map: Map = .{}; + inline for (@typeInfo(Replacements).Struct.fields) |field| { + try map.put(alloc, field.name, @field(replacements, field.name)); + } + + var out: ArrayListUnmanaged(u8) = .{}; + const w = out.writer(alloc); + + var i: usize = 0; + while (mem.indexOfScalarPos(u8, template, i, '{')) |open_at| { + const end = mem.indexOfScalarPos(u8, template, open_at + 1, '}') orelse break; + const inner = template[open_at + 1 .. end]; + + if (map.get(inner)) |replacement| { + // write previous + try w.writeAll(template[i..open_at]); + // write template + try w.writeAll(replacement); + // move one past the ending '}' + i = end + 1; + } else { + // write up to and including the { + try w.writeAll(template[i .. open_at + 1]); + // move one past it + i = open_at + 1; + } + } + + // write ending + try w.writeAll(template[i..]); + + return out.items; +} + +fn writeHtmlEscapedSlice(text: []const u8, writer: anytype, escape_quotes: bool) !void { + var i: usize = 0; + while (mem.indexOfAnyPos( + u8, + text, + i, + if (escape_quotes) + &.{ '&', '<', '>', '"' } + else + &.{ '&', '<', '>' }, + )) |at| { + const replace = switch (text[at]) { + '&' => "&", + '<' => "<", + '>' => ">", + '"' => """, + else => unreachable, + }; + try writer.writeAll(text[i..at]); + try writer.writeAll(replace); + i = at + 1; + } + try writer.writeAll(text[i..]); +} diff --git a/test/behavior.zig b/test/behavior.zig new file mode 100644 index 0000000000000000000000000000000000000000..373f1d4ea5ec37a852c51a4b679c12c120124d59 --- /dev/null +++ b/test/behavior.zig @@ -0,0 +1,393 @@ +//! This program executes behavior tests found in ./test/behavior. Additionally, it can be run +//! as a standalone program to compile and execute a single behavior test (for debugging) + +/// 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, +}; + +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: core.Comp = .{ + .gpa = tracer.tracingAllocator(&alloc), + .options = .{ .dump_ir = case_options.dump_ir }, + .backend = .{ .js = &backend }, + .out = .{ .writer = out, .tty = .escape_codes }, + }; + try comp.init(); + + 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.dumpToWriter(&comp, out, .escape_codes); + 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.semaEntryPoint(main_file); + + 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"); + + 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; + } +} + +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]"; + + if (case_options.sync) std.debug.lockStdErr(); + defer if (case_options.sync) std.debug.unlockStdErr(); + + 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 args = try std.process.argsAlloc(alloc); + defer std.process.argsFree(alloc, args); + + var test_root: []const u8 = "test/behavior"; + var case_options: CaseOptions = .{}; + var all_logs = false; + var filter: ?[]const u8 = null; + defer if (filter) |f| alloc.free(f); + + var argument_error = false; + + for (args[1..]) |full_arg| { + if (std.mem.startsWith(u8, full_arg, "--")) { + if (getFlag("--test-root", full_arg)) |flag| { + test_root = flag; + } else if (std.mem.eql(u8, "--dump-ir", full_arg)) { + case_options.dump_ir = true; + } else if (std.mem.eql(u8, "--all-logs", full_arg)) { + all_logs = true; + } else if (std.mem.eql(u8, "--sync", full_arg)) { + case_options.sync = true; + } else { + std.debug.print("Unknown argument: {s}\n", .{full_arg}); + argument_error = true; + } + } else { + if (filter != null) { + std.debug.print("Unknown positional argument: {s}\n", .{full_arg}); + } else { + filter = try std.mem.replaceOwned( + u8, + alloc, + full_arg, + if (std.fs.path.sep == '/') "\\" else '/', + std.fs.path.sep_str, + ); + } + } + } + + if (argument_error) std.process.exit(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\n", .{}); + } + 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(); + + for (test_cases.items) |*case| { + if (case.success) { + if (all_logs) { + std.debug.print("PASS: {s}\n", .{case.display}); + 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); + } +} + +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 core = @import("core"); +const qjs = @import("quickjs"); + +const tracer = @import("tracer"); +pub const tracer_impl = @field(tracer, @import("build_options").tracing); diff --git a/test/behavior/arithmetic b/test/behavior/arithmetic new file mode 100644 index 0000000000000000000000000000000000000000..59a036d83dd160c3025a3258a29f683a93bd6b12 --- /dev/null +++ b/test/behavior/arithmetic @@ -0,0 +1,67 @@ +fn f(a: float) -> float { a } + +fn addFloat(a: float, b: float) -> float { + a + b +} + +fn subFloat(a: float, b: float) -> float { + a - b +} + +fn mulFloat(a: float, b: float) -> float { + a * b +} + +fn divFloat(a: float, b: float) -> float { + a / b +} + +// fn modFloat(a: float, b: float) -> float { +// a % b +// } + +// fn expFloat(a: float, b: float) -> float { +// a ** b +// } + +fn i(a: int) -> int { a } + +fn addInt(a: int, b: int) -> int { + a + b +} + +fn subInt(a: int, b: int) -> int { + a - b +} + +// fn mulInt(a: int, b: int) -> int { +// a * b +// } + +// fn divInt(a: int, b: int) -> int { +// a / b +// } + +// fn expInt(a: int, b: int) -> int { +// a * b +// } + +// fn modFloat(a: float, b: float) -> float { +// a % b +// } + +let assertEqual = @js("assertEqual"); + +assertEqual(addFloat(1, 2), f(3)); +assertEqual(addFloat(1.5, 2.2), f(3.7)); +assertEqual(subFloat(1, 2), f(-1)); +assertEqual(subFloat(1.5, 2.2), f(-0.7000000000000002)); +assertEqual(mulFloat(1, 2), f(2)); +assertEqual(mulFloat(1.5, 2.2), f(3.3000000000000003)); +assertEqual(divFloat(1, 2), f(0.5)); +assertEqual(divFloat(1.5, 2.2), f(0.6818181818181818)); + +assertEqual(addInt(1, 2), i(3)); +assertEqual(addInt(-421, 592), i(171)); +assertEqual(subInt(1, 2), i(-1)); +assertEqual(subInt(528, -43), i(571)); -- 2.54.0