diff --git a/Cargo.lock b/Cargo.lock index 1ce9784ff3..e3f454df96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5280,6 +5280,21 @@ dependencies = [ "getrandom", ] +[[package]] +name = "valgrind" +version = "0.1.0" +dependencies = [ + "bumpalo", + "cli_utils", + "roc_build", + "roc_cli", + "roc_load", + "roc_mono", + "roc_packaging", + "roc_reporting", + "target-lexicon", +] + [[package]] name = "valuable" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index b007bf7624..4a38ec1f7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/repl_wasm", "crates/repl_expect", "crates/test_utils", + "crates/valgrind", "crates/tracing", "crates/utils", "crates/docs", diff --git a/crates/valgrind/Cargo.toml b/crates/valgrind/Cargo.toml new file mode 100644 index 0000000000..77230f3406 --- /dev/null +++ b/crates/valgrind/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "valgrind" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[dependencies] +roc_cli = { path = "../cli" } # depend on CLI to prevent cyclic dependencies +cli_utils = { path = "../cli_utils" } +roc_build = { path = "../compiler/build" } +roc_mono = { path = "../compiler/mono" } +roc_load = { path = "../compiler/load" } +roc_reporting = { path = "../reporting" } +roc_packaging = { path = "../packaging" } +bumpalo.workspace = true +target-lexicon.workspace = true + +[build-dependencies] +roc_cli = { path = "../cli" } # depend on CLI to prevent cyclic dependencies +cli_utils = { path = "../cli_utils" } diff --git a/crates/valgrind/build.rs b/crates/valgrind/build.rs new file mode 100644 index 0000000000..7d40fc68d3 --- /dev/null +++ b/crates/valgrind/build.rs @@ -0,0 +1,22 @@ +fn main() { + // goal: build the platform, so tests can use `precompiled-platform=true` + + // first, make sure the roc binary is up to data + // cli_utils::helpers::build_roc_bin_cached(); + + // next, we just compile one of our examples + let roc_path = std::path::Path::new("../../target/debug/roc"); + let out = cli_utils::helpers::run_roc_with_stdin_and_env( + roc_path, + &["run", "tests/str_concat_1.roc"], + &[], + &[], + ); + + if !out.status.success() { + panic!( + "Building platform with `{:?}` failed!\n\n{}\n\n{}", + out.cmd_str, out.stdout, out.stderr + ); + } +} diff --git a/crates/valgrind/src/lib.rs b/crates/valgrind/src/lib.rs new file mode 100644 index 0000000000..2c2c37c27b --- /dev/null +++ b/crates/valgrind/src/lib.rs @@ -0,0 +1,101 @@ +#![cfg(test)] + +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use roc_build::{ + link::{LinkType, LinkingStrategy}, + program::{CodeGenBackend, CodeGenOptions}, +}; +use roc_cli::build::{BuildOrdering, BuiltFile}; +use roc_load::Threading; +use roc_mono::ir::OptLevel; + +fn list_files(dir: &std::path::Path) -> std::vec::Vec { + std::fs::read_dir(dir) + .unwrap() + .map(|f| PathBuf::from(f.unwrap().file_name())) + .collect::>() +} + +fn run_example(app_module_path: impl AsRef) { + let app_module_path = app_module_path.as_ref(); + + let arena = bumpalo::Bump::new(); + let triple = target_lexicon::Triple::host(); + + let code_gen_options = CodeGenOptions { + backend: CodeGenBackend::Llvm, + opt_level: OptLevel::Normal, + emit_debug_info: false, + }; + + let emit_timings = false; + let link_type = LinkType::Executable; + let linking_strategy = LinkingStrategy::Surgical; + let prebuilt_requested = true; + let wasm_dev_stack_bytes = None; + + let roc_cache_dir = roc_packaging::cache::RocCacheDir::Disallowed; + let build_ordering = BuildOrdering::AlwaysBuild; + + let res_binary_path = roc_cli::build::build_file( + &arena, + &triple, + PathBuf::from(app_module_path), + code_gen_options, + emit_timings, + link_type, + linking_strategy, + prebuilt_requested, + Threading::AtMost(2), + wasm_dev_stack_bytes, + roc_cache_dir, + build_ordering, + ); + + match res_binary_path { + Ok(BuiltFile { + binary_path, + problems, + total_time: _, + expect_metadata: _, + }) => { + if problems.exit_code() != 0 { + panic!("there are problems") + } + // If possible, report the generated executable name relative to the current dir. + let generated_filename = binary_path + .strip_prefix(std::env::current_dir().unwrap()) + .unwrap_or(&binary_path) + .to_str() + .unwrap(); + + let (out, _raw_xml) = + cli_utils::helpers::run_with_valgrind([], &[generated_filename.to_string()]); + + if !out.status.success() { + panic!( + "Running the application `{:?}` failed!\n\n{}\n\n{}", + out.cmd_str, out.stdout, out.stderr + ); + } + } + Err(e) => panic!("{:?}", e), + } +} + +#[test] +fn it_works() { + for dir_entry in std::fs::read_dir("tests").unwrap() { + let path = dir_entry.unwrap().path(); + + if path.extension() != Some(OsStr::new("roc")) { + continue; + } + + run_example(path); + } +} diff --git a/crates/valgrind/tests/rocLovesZig b/crates/valgrind/tests/rocLovesZig new file mode 100755 index 0000000000..e57e7f151b Binary files /dev/null and b/crates/valgrind/tests/rocLovesZig differ diff --git a/crates/valgrind/tests/str_concat_1.roc b/crates/valgrind/tests/str_concat_1.roc new file mode 100644 index 0000000000..5424e2bdfb --- /dev/null +++ b/crates/valgrind/tests/str_concat_1.roc @@ -0,0 +1,8 @@ +app "test" + packages { pf: "../zig-platform/main.roc" } + imports [] + provides [main] to pf + +main = + Str.withCapacity 42 + |> Str.concat "foobar" diff --git a/crates/valgrind/zig-platform/dynhost b/crates/valgrind/zig-platform/dynhost new file mode 100755 index 0000000000..9b44a8cc63 Binary files /dev/null and b/crates/valgrind/zig-platform/dynhost differ diff --git a/crates/valgrind/zig-platform/host.zig b/crates/valgrind/zig-platform/host.zig new file mode 100644 index 0000000000..7465a1fe0e --- /dev/null +++ b/crates/valgrind/zig-platform/host.zig @@ -0,0 +1,138 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const str = @import("str"); +const RocStr = str.RocStr; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expect = testing.expect; + +comptime { + // This is a workaround for https://github.com/ziglang/zig/issues/8218 + // which is only necessary on macOS. + // + // Once that issue is fixed, we can undo the changes in + // 177cf12e0555147faa4d436e52fc15175c2c4ff0 and go back to passing + // -fcompiler-rt in link.rs instead of doing this. Note that this + // workaround is present in many host.zig files, so make sure to undo + // it everywhere! + if (builtin.os.tag == .macos) { + _ = @import("compiler_rt"); + } +} + +const Align = 2 * @alignOf(usize); +extern fn malloc(size: usize) callconv(.C) ?*align(Align) anyopaque; +extern fn realloc(c_ptr: [*]align(Align) u8, size: usize) callconv(.C) ?*anyopaque; +extern fn free(c_ptr: [*]align(Align) u8) callconv(.C) void; +extern fn memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void; +extern fn memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void; + +const DEBUG: bool = false; + +export fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + var ptr = malloc(size); + const stdout = std.io.getStdOut().writer(); + stdout.print("alloc: {d} (alignment {d}, size {d})\n", .{ ptr, alignment, size }) catch unreachable; + return ptr; + } else { + return malloc(size); + } +} + +export fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("realloc: {d} (alignment {d}, old_size {d})\n", .{ c_ptr, alignment, old_size }) catch unreachable; + } + + return realloc(@alignCast(Align, @ptrCast([*]u8, c_ptr)), new_size); +} + +export fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void { + if (DEBUG) { + const stdout = std.io.getStdOut().writer(); + stdout.print("dealloc: {d} (alignment {d})\n", .{ c_ptr, alignment }) catch unreachable; + } + + free(@alignCast(Align, @ptrCast([*]u8, c_ptr))); +} + +export fn roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { + _ = tag_id; + + const stderr = std.io.getStdErr().writer(); + const msg = @ptrCast([*:0]const u8, c_ptr); + stderr.print("Application crashed with message\n\n {s}\n\nShutting down\n", .{msg}) catch unreachable; + std.process.exit(0); +} + +export fn roc_memcpy(dst: [*]u8, src: [*]u8, size: usize) callconv(.C) void { + return memcpy(dst, src, size); +} + +export fn roc_memset(dst: [*]u8, value: i32, size: usize) callconv(.C) void { + return memset(dst, value, size); +} + +extern fn kill(pid: c_int, sig: c_int) c_int; +extern fn shm_open(name: *const i8, oflag: c_int, mode: c_uint) c_int; +extern fn mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) *anyopaque; +extern fn getppid() c_int; + +fn roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn roc_mmap(addr: ?*anyopaque, length: c_uint, prot: c_int, flags: c_int, fd: c_int, offset: c_uint) callconv(.C) *anyopaque { + return mmap(addr, length, prot, flags, fd, offset); +} + +comptime { + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } +} + +const mem = std.mem; +const Allocator = mem.Allocator; + +extern fn roc__mainForHost_1_exposed_generic(*RocStr) void; + +const Unit = extern struct {}; + +pub fn main() u8 { + const stdout = std.io.getStdOut().writer(); + const stderr = std.io.getStdErr().writer(); + + var timer = std.time.Timer.start() catch unreachable; + + // actually call roc to populate the callresult + var callresult = RocStr.empty(); + roc__mainForHost_1_exposed_generic(&callresult); + + const nanos = timer.read(); + const seconds = (@intToFloat(f64, nanos) / 1_000_000_000.0); + + // stdout the result + stdout.print("{s}", .{callresult.asSlice()}) catch unreachable; + + callresult.deinit(); + + stderr.print("runtime: {d:.3}ms\n", .{seconds * 1000}) catch unreachable; + + return 0; +} diff --git a/crates/valgrind/zig-platform/libapp.so b/crates/valgrind/zig-platform/libapp.so new file mode 100644 index 0000000000..ec5fd77dd0 Binary files /dev/null and b/crates/valgrind/zig-platform/libapp.so differ diff --git a/crates/valgrind/zig-platform/main.roc b/crates/valgrind/zig-platform/main.roc new file mode 100644 index 0000000000..a52fe9a480 --- /dev/null +++ b/crates/valgrind/zig-platform/main.roc @@ -0,0 +1,9 @@ +platform "echo-in-zig" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main