diff --git a/Cargo.lock b/Cargo.lock index ecc5b2ee0f9..fa38e674544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -471,6 +471,7 @@ dependencies = [ "rlimit", "roc_collections", "roc_command_utils", + "roc_error_macros", "roc_load", "roc_module", "roc_reporting", diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index e7acac39e30..f68bdb8f02d 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -65,7 +65,6 @@ pub const FLAG_TARGET: &str = "target"; pub const FLAG_TIME: &str = "time"; pub const FLAG_VERBOSE: &str = "verbose"; pub const FLAG_LINKER: &str = "linker"; -pub const FLAG_PREBUILT: &str = "prebuilt-platform"; pub const FLAG_CHECK: &str = "check"; pub const FLAG_STDIN: &str = "stdin"; pub const FLAG_STDOUT: &str = "stdout"; @@ -78,6 +77,9 @@ pub const GLUE_DIR: &str = "GLUE_DIR"; pub const GLUE_SPEC: &str = "GLUE_SPEC"; pub const DIRECTORY_OR_FILES: &str = "DIRECTORY_OR_FILES"; pub const ARGS_FOR_APP: &str = "ARGS_FOR_APP"; +pub const FLAG_PP_HOST: &str = "host"; +pub const FLAG_PP_PLATFORM: &str = "platform"; +pub const FLAG_PP_DYLIB: &str = "lib"; const VERSION: &str = include_str!("../../../version.txt"); const DEFAULT_GENERATED_DOCS_DIR: &str = "generated-docs"; @@ -131,12 +133,6 @@ pub fn build_app() -> Command { .value_parser(["surgical", "legacy"]) .required(false); - let flag_prebuilt = Arg::new(FLAG_PREBUILT) - .long(FLAG_PREBUILT) - .help("Assume the platform has been prebuilt and skip rebuilding the platform\n(This is enabled implicitly when using `roc build` with a --target other than `--target `, unless the target is wasm.)") - .action(ArgAction::SetTrue) - .required(false); - let flag_wasm_stack_size_kb = Arg::new(FLAG_WASM_STACK_SIZE_KB) .long(FLAG_WASM_STACK_SIZE_KB) .help("Stack size in kilobytes for wasm32 target\n(This only applies when --dev also provided.)") @@ -184,7 +180,6 @@ pub fn build_app() -> Command { .arg(flag_profiling.clone()) .arg(flag_time.clone()) .arg(flag_linker.clone()) - .arg(flag_prebuilt.clone()) .arg(flag_fuzz.clone()) .arg(flag_wasm_stack_size_kb) .arg( @@ -235,7 +230,6 @@ pub fn build_app() -> Command { .arg(flag_profiling.clone()) .arg(flag_time.clone()) .arg(flag_linker.clone()) - .arg(flag_prebuilt.clone()) .arg(flag_fuzz.clone()) .arg( Arg::new(FLAG_VERBOSE) @@ -266,7 +260,6 @@ pub fn build_app() -> Command { .arg(flag_profiling.clone()) .arg(flag_time.clone()) .arg(flag_linker.clone()) - .arg(flag_prebuilt.clone()) .arg(flag_fuzz.clone()) .arg(roc_file_to_run.clone()) .arg(args_for_app.clone().last(true)) @@ -281,7 +274,6 @@ pub fn build_app() -> Command { .arg(flag_profiling.clone()) .arg(flag_time.clone()) .arg(flag_linker.clone()) - .arg(flag_prebuilt.clone()) .arg(flag_fuzz.clone()) .arg(roc_file_to_run.clone()) .arg(args_for_app.clone().last(true)) @@ -371,28 +363,23 @@ pub fn build_app() -> Command { .default_value(DEFAULT_ROC_FILENAME) ) ) - .subcommand(Command::new(CMD_GEN_STUB_LIB) - .about("Generate a stubbed shared library that can be used for linking a platform binary.\nThe stubbed library has prototypes, but no function bodies.\n\nNote: This command will be removed in favor of just using `roc build` once all platforms support the surgical linker") + .subcommand(Command::new(CMD_PREPROCESS_HOST) + .about("Runs the surgical linker pre-processor to generate `.rh` and `.rm` files.") .arg( - Arg::new(ROC_FILE) - .help("The .roc file for an app using the platform") + Arg::new(FLAG_PP_HOST) + .help("Path to the host executable where the app was linked dynamically") .value_parser(value_parser!(PathBuf)) .required(true) ) .arg( - Arg::new(FLAG_TARGET) - .long(FLAG_TARGET) - .help("Choose a different target") - .default_value(Into::<&'static str>::into(Target::default())) - .value_parser(build_target_values_parser.clone()) - .required(false), + Arg::new(FLAG_PP_PLATFORM) + .help("Path to the platform/main.roc file") + .value_parser(value_parser!(PathBuf)) + .required(true) ) - ) - .subcommand(Command::new(CMD_PREPROCESS_HOST) - .about("Runs the surgical linker preprocessor to generate `.rh` and `.rm` files.") .arg( - Arg::new(ROC_FILE) - .help("The .roc file for an app using the platform") + Arg::new(FLAG_PP_DYLIB) + .help("Path to a stubbed app dynamic library (e.g. roc build --lib app.roc)") .value_parser(value_parser!(PathBuf)) .required(true) ) @@ -404,6 +391,13 @@ pub fn build_app() -> Command { .value_parser(build_target_values_parser) .required(false), ) + .arg( + Arg::new(FLAG_VERBOSE) + .long(FLAG_VERBOSE) + .help("Print detailed information while pre-processing host") + .action(ArgAction::SetTrue) + .required(false) + ) ) .arg(flag_optimize) .arg(flag_max_threads) @@ -413,7 +407,6 @@ pub fn build_app() -> Command { .arg(flag_profiling) .arg(flag_time) .arg(flag_linker) - .arg(flag_prebuilt) .arg(flag_fuzz) .arg(roc_file_to_run) .arg(args_for_app.trailing_var_arg(true)) @@ -822,18 +815,6 @@ pub fn build( LinkingStrategy::Surgical }; - let prebuilt = { - let cross_compile = target != Target::default(); - let targeting_wasm = matches!(target.architecture(), Architecture::Wasm32); - - matches.get_flag(FLAG_PREBUILT) || - // When compiling for a different target, assume a prebuilt platform. - // Otherwise compilation would most likely fail because many toolchains - // assume you're compiling for the current machine. We make an exception - // for Wasm, because cross-compiling is the norm in that case. - (cross_compile && !targeting_wasm) - }; - let fuzz = matches.get_flag(FLAG_FUZZ); if fuzz && !matches!(code_gen_backend, CodeGenBackend::Llvm(_)) { user_error!("Cannot instrument binary for fuzzing while using a dev backend."); @@ -868,7 +849,6 @@ pub fn build( emit_timings, link_type, linking_strategy, - prebuilt, wasm_dev_stack_bytes, roc_cache_dir, load_config, diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 06e101fa7f6..dd4e5cb8e5f 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -4,16 +4,19 @@ use roc_build::link::LinkType; use roc_build::program::{check_file, CodeGenBackend}; use roc_cli::{ build_app, format_files, format_src, test, BuildConfig, FormatMode, CMD_BUILD, CMD_CHECK, - CMD_DEV, CMD_DOCS, CMD_FORMAT, CMD_GEN_STUB_LIB, CMD_GLUE, CMD_PREPROCESS_HOST, CMD_REPL, - CMD_RUN, CMD_TEST, CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_DEV, FLAG_LIB, - FLAG_NO_LINK, FLAG_OUTPUT, FLAG_STDIN, FLAG_STDOUT, FLAG_TARGET, FLAG_TIME, GLUE_DIR, - GLUE_SPEC, ROC_FILE, + CMD_DEV, CMD_DOCS, CMD_FORMAT, CMD_GLUE, CMD_PREPROCESS_HOST, CMD_REPL, CMD_RUN, CMD_TEST, + CMD_VERSION, DIRECTORY_OR_FILES, FLAG_CHECK, FLAG_DEV, FLAG_LIB, FLAG_NO_LINK, FLAG_OUTPUT, + FLAG_PP_DYLIB, FLAG_PP_HOST, FLAG_PP_PLATFORM, FLAG_STDIN, FLAG_STDOUT, FLAG_TARGET, FLAG_TIME, + FLAG_VERBOSE, GLUE_DIR, GLUE_SPEC, ROC_FILE, }; use roc_docs::generate_docs_html; -use roc_error_macros::user_error; + +#[allow(unused_imports)] +use roc_error_macros::{internal_error, user_error}; + use roc_gen_dev::AssemblyBackendMode; use roc_gen_llvm::llvm::build::LlvmBackendMode; -use roc_load::{FunctionKind, LoadingProblem, Threading}; +use roc_load::{LoadingProblem, Threading}; use roc_packaging::cache::{self, RocCacheDir}; use roc_target::Target; use std::fs::{self, FileType}; @@ -120,47 +123,55 @@ fn main() -> io::Result<()> { Ok(1) } } - Some((CMD_GEN_STUB_LIB, matches)) => { - let input_path = matches.get_one::(ROC_FILE).unwrap(); - let target = matches - .get_one::(FLAG_TARGET) - .and_then(|s| Target::from_str(s).ok()) - .unwrap_or_default(); - let function_kind = FunctionKind::LambdaSet; - roc_linker::generate_stub_lib( - input_path, - RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), - target, - function_kind, - ); - Ok(0) - } Some((CMD_PREPROCESS_HOST, matches)) => { - let input_path = matches.get_one::(ROC_FILE).unwrap(); + let preprocess_host_err = + { |msg: String| user_error!("\n\n ERROR PRE-PROCESSING HOST: {}\n\n", msg) }; + + let host_path = matches.get_one::(FLAG_PP_HOST).unwrap(); + if !host_path.is_file() { + preprocess_host_err(format!( + "Expected to find the host executable file at {}", + &host_path.display() + )); + } + + let platform_path = matches.get_one::(FLAG_PP_PLATFORM).unwrap(); + if !platform_path.is_file() { + preprocess_host_err(format!( + "Expected to find the platform/main.roc file at {}", + &platform_path.display() + )); + } + + let dylib_path = matches.get_one::(FLAG_PP_DYLIB).unwrap(); + if !dylib_path.is_file() { + preprocess_host_err(format!( + "Expected to find the app stub dynamic library file at {}", + dylib_path.display() + )); + } + let target = matches .get_one::(FLAG_TARGET) .and_then(|s| Target::from_str(s).ok()) .unwrap_or_default(); - let function_kind = FunctionKind::LambdaSet; - let (platform_path, stub_lib, stub_dll_symbols) = roc_linker::generate_stub_lib( - input_path, - RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), - target, - function_kind, - ); + let verbose_and_time = matches.get_one::(FLAG_VERBOSE).unwrap(); + + #[cfg(target_os = "windows")] + { + internal_error!("TODO populate stub_dll_symbols for Windows"); + } - // TODO: pipeline the executable location through here. - // Currently it is essentally hardcoded as platform_path/dynhost. roc_linker::preprocess_host( target, - &platform_path.with_file_name("main.roc"), - // The target triple string must be derived from the triple to convert from the generic - // `system` target to the exact specific target. - &platform_path.with_file_name(format!("{}.rh", target)), - &stub_lib, - &stub_dll_symbols, + host_path, + platform_path, + dylib_path, + *verbose_and_time, + *verbose_and_time, ); + Ok(0) } Some((CMD_BUILD, matches)) => { diff --git a/crates/cli/tests/algorithms/fibonacci/.gitignore b/crates/cli/tests/algorithms/fibonacci/.gitignore new file mode 100644 index 00000000000..f6dd3089d9c --- /dev/null +++ b/crates/cli/tests/algorithms/fibonacci/.gitignore @@ -0,0 +1,7 @@ +zig-cache/ +zig-out/ +glue/ + +build + +*.a \ No newline at end of file diff --git a/crates/cli/tests/algorithms/fibonacci.roc b/crates/cli/tests/algorithms/fibonacci/app.roc similarity index 75% rename from crates/cli/tests/algorithms/fibonacci.roc rename to crates/cli/tests/algorithms/fibonacci/app.roc index 906475c98dc..7483fbcf89e 100644 --- a/crates/cli/tests/algorithms/fibonacci.roc +++ b/crates/cli/tests/algorithms/fibonacci/app.roc @@ -1,5 +1,5 @@ -app "fibonacci" - packages { pf: "fibonacci-platform/main.roc" } +app "" + packages { pf: "platform.roc" } imports [] provides [main] to pf diff --git a/crates/cli/tests/algorithms/fibonacci/build.roc b/crates/cli/tests/algorithms/fibonacci/build.roc new file mode 100644 index 00000000000..84b2a6931f9 --- /dev/null +++ b/crates/cli/tests/algorithms/fibonacci/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = Help.prebuiltBinaryName target + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "glue/", "platform.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libfibonacci.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/algorithms/fibonacci/build.zig b/crates/cli/tests/algorithms/fibonacci/build.zig new file mode 100644 index 00000000000..1896f1bfa73 --- /dev/null +++ b/crates/cli/tests/algorithms/fibonacci/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "fibonacci", + .root_source_file = .{ .path = "host.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/algorithms/fibonacci-platform/host.zig b/crates/cli/tests/algorithms/fibonacci/host.zig similarity index 99% rename from crates/cli/tests/algorithms/fibonacci-platform/host.zig rename to crates/cli/tests/algorithms/fibonacci/host.zig index e75b1dd7a01..fc1a88eff27 100644 --- a/crates/cli/tests/algorithms/fibonacci-platform/host.zig +++ b/crates/cli/tests/algorithms/fibonacci/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/algorithms/fibonacci-platform/main.roc b/crates/cli/tests/algorithms/fibonacci/platform.roc similarity index 88% rename from crates/cli/tests/algorithms/fibonacci-platform/main.roc rename to crates/cli/tests/algorithms/fibonacci/platform.roc index daaf2ff5c85..963f2a45b0b 100644 --- a/crates/cli/tests/algorithms/fibonacci-platform/main.roc +++ b/crates/cli/tests/algorithms/fibonacci/platform.roc @@ -1,4 +1,4 @@ -platform "fibonacci" +platform "" requires {} { main : I64 -> I64 } exposes [] packages {} diff --git a/crates/cli/tests/algorithms/quicksort/.gitignore b/crates/cli/tests/algorithms/quicksort/.gitignore new file mode 100644 index 00000000000..f6dd3089d9c --- /dev/null +++ b/crates/cli/tests/algorithms/quicksort/.gitignore @@ -0,0 +1,7 @@ +zig-cache/ +zig-out/ +glue/ + +build + +*.a \ No newline at end of file diff --git a/crates/cli/tests/algorithms/quicksort.roc b/crates/cli/tests/algorithms/quicksort/app.roc similarity index 97% rename from crates/cli/tests/algorithms/quicksort.roc rename to crates/cli/tests/algorithms/quicksort/app.roc index 828295a8b3e..789c59061f5 100644 --- a/crates/cli/tests/algorithms/quicksort.roc +++ b/crates/cli/tests/algorithms/quicksort/app.roc @@ -1,5 +1,5 @@ app "quicksort" - packages { pf: "quicksort-platform/main.roc" } + packages { pf: "platform.roc" } imports [] provides [quicksort] to pf diff --git a/crates/cli/tests/algorithms/quicksort/build.roc b/crates/cli/tests/algorithms/quicksort/build.roc new file mode 100644 index 00000000000..baded255d10 --- /dev/null +++ b/crates/cli/tests/algorithms/quicksort/build.roc @@ -0,0 +1,57 @@ +app "build-quicksort" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = Help.prebuiltBinaryName target + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "glue/", "platform.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libquicksort.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/algorithms/quicksort/build.zig b/crates/cli/tests/algorithms/quicksort/build.zig new file mode 100644 index 00000000000..a0970ca8fad --- /dev/null +++ b/crates/cli/tests/algorithms/quicksort/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "quicksort", + .root_source_file = .{ .path = "host.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/algorithms/quicksort-platform/host.zig b/crates/cli/tests/algorithms/quicksort/host.zig similarity index 99% rename from crates/cli/tests/algorithms/quicksort-platform/host.zig rename to crates/cli/tests/algorithms/quicksort/host.zig index 50362d5f5cc..bfd8bd71321 100644 --- a/crates/cli/tests/algorithms/quicksort-platform/host.zig +++ b/crates/cli/tests/algorithms/quicksort/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/algorithms/quicksort-platform/main.roc b/crates/cli/tests/algorithms/quicksort/platform.roc similarity index 100% rename from crates/cli/tests/algorithms/quicksort-platform/main.roc rename to crates/cli/tests/algorithms/quicksort/platform.roc diff --git a/crates/cli/tests/benchmarks/.gitignore b/crates/cli/tests/benchmarks/.gitignore new file mode 100644 index 00000000000..858e85ce8f7 --- /dev/null +++ b/crates/cli/tests/benchmarks/.gitignore @@ -0,0 +1,7 @@ +zig-cache/ +zig-out/ +host/glue/ + +build + +*.a \ No newline at end of file diff --git a/crates/cli/tests/benchmarks/build.roc b/crates/cli/tests/benchmarks/build.roc new file mode 100644 index 00000000000..99202dd20ff --- /dev/null +++ b/crates/cli/tests/benchmarks/build.roc @@ -0,0 +1,57 @@ +app "build-quicksort" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/benchmarks/build.zig b/crates/cli/tests/benchmarks/build.zig new file mode 100644 index 00000000000..2f552b38752 --- /dev/null +++ b/crates/cli/tests/benchmarks/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/host.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/benchmarks/platform/host.zig b/crates/cli/tests/benchmarks/host/host.zig similarity index 99% rename from crates/cli/tests/benchmarks/platform/host.zig rename to crates/cli/tests/benchmarks/host/host.zig index f227605d280..6e2a7d0d966 100644 --- a/crates/cli/tests/benchmarks/platform/host.zig +++ b/crates/cli/tests/benchmarks/host/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/benchmarks/platform/Task.roc b/crates/cli/tests/benchmarks/platform/Task.roc index 16da2d1eb73..3ddfbed5787 100644 --- a/crates/cli/tests/benchmarks/platform/Task.roc +++ b/crates/cli/tests/benchmarks/platform/Task.roc @@ -1,6 +1,6 @@ interface Task exposes [Task, succeed, fail, after, map, putLine, putInt, getInt, forever, loop, attempt] - imports [pf.Effect] + imports [Effect] Task ok err : Effect.Effect (Result ok err) diff --git a/crates/cli/tests/cli/build.roc b/crates/cli/tests/cli/build.roc new file mode 100644 index 00000000000..0f416481703 --- /dev/null +++ b/crates/cli/tests/cli/build.roc @@ -0,0 +1,7 @@ +app "" + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } + imports [pf.Task] + provides [main] to pf + +# does nothing, but here to satisfy the test runner +main = Task.ok {} diff --git a/examples/helloWorld.roc b/crates/cli/tests/cli/helloWorld.roc similarity index 100% rename from examples/helloWorld.roc rename to crates/cli/tests/cli/helloWorld.roc diff --git a/crates/cli/tests/cli/ingested-file-bytes-no-ann.roc b/crates/cli/tests/cli/ingested-file-bytes-no-ann.roc index aed8328bd8b..f76f9f10df0 100644 --- a/crates/cli/tests/cli/ingested-file-bytes-no-ann.roc +++ b/crates/cli/tests/cli/ingested-file-bytes-no-ann.roc @@ -1,10 +1,10 @@ app [main] { pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } import pf.Stdout -import "ingested-file.roc" as license +import "ingested-file.roc" as ingestedFile main = - license + ingestedFile |> List.map Num.toU64 |> List.sum |> Num.toStr diff --git a/crates/cli/tests/cli_run.rs b/crates/cli/tests/cli_run.rs index f457fb47d52..2fdb26c17b8 100644 --- a/crates/cli/tests/cli_run.rs +++ b/crates/cli/tests/cli_run.rs @@ -10,9 +10,9 @@ extern crate roc_module; #[cfg(test)] mod cli_run { use cli_utils::helpers::{ - extract_valgrind_errors, file_path_from_root, fixture_file, fixtures_dir, has_error, - known_bad_file, run_cmd, run_roc, run_with_valgrind, Out, ValgrindError, - ValgrindErrorXWhat, + dir_path_from_root, extract_valgrind_errors, file_path_from_root, fixture_file, + fixtures_dir, has_error, known_bad_file, rebuild_host, run_cmd, run_roc, run_with_valgrind, + Out, ValgrindError, ValgrindErrorXWhat, }; use const_format::concatcp; use indoc::indoc; @@ -22,7 +22,7 @@ mod cli_run { use roc_test_utils::assert_multiline_str_eq; use serial_test::serial; use std::iter; - use std::path::Path; + use std::path::{Path, PathBuf}; #[cfg(all(unix, not(target_os = "macos")))] const ALLOW_VALGRIND: bool = true; @@ -54,8 +54,7 @@ mod cli_run { const OPTIMIZE_FLAG: &str = concatcp!("--", roc_cli::FLAG_OPTIMIZE); const LINKER_FLAG: &str = concatcp!("--", roc_cli::FLAG_LINKER); const CHECK_FLAG: &str = concatcp!("--", roc_cli::FLAG_CHECK); - #[allow(dead_code)] - const PREBUILT_PLATFORM: &str = concatcp!("--", roc_cli::FLAG_PREBUILT); + #[allow(dead_code)] const TARGET_FLAG: &str = concatcp!("--", roc_cli::FLAG_TARGET); @@ -157,6 +156,7 @@ mod cli_run { #[allow(clippy::too_many_arguments)] fn check_output_with_stdin( file: &Path, + dir_name: &PathBuf, stdin: &[&str], flags: &[&str], roc_app_args: &[String], @@ -165,6 +165,8 @@ mod cli_run { use_valgrind: UseValgrind, test_cli_commands: TestCliCommands, ) { + rebuild_host(dir_name, &file.to_path_buf()); + // valgrind does not yet support avx512 instructions, see #1963. // we can't enable this only when testing with valgrind because of host re-use between tests #[cfg(any(target_arch = "x86", target_arch = "x86_64"))] @@ -330,6 +332,7 @@ mod cli_run { let path = file_path_from_root(dir_name, roc_filename); check_output_with_stdin( &path, + &PathBuf::from(dir_name), &[], flags, &[], @@ -373,6 +376,8 @@ mod cli_run { let file_name = file_path_from_root(dir_name, roc_filename); let mut roc_app_args: Vec = Vec::new(); + cli_utils::helpers::rebuild_host(&PathBuf::from(dir_name), &file_name); + for arg in args { match arg { Arg::ExamplePath(file) => { @@ -433,6 +438,7 @@ mod cli_run { // Check with and without optimizations check_output_with_stdin( &file_name, + &PathBuf::from(dir_name), stdin, &custom_flags, &roc_app_args, @@ -448,6 +454,7 @@ mod cli_run { #[cfg(not(debug_assertions))] check_output_with_stdin( &file_name, + &PathBuf::from(dir_name), stdin, &custom_flags, &roc_app_args, @@ -462,6 +469,7 @@ mod cli_run { if TEST_LEGACY_LINKER { check_output_with_stdin( &file_name, + &PathBuf::from(dir_name), stdin, &[LINKER_FLAG, "legacy"], &roc_app_args, @@ -478,7 +486,7 @@ mod cli_run { #[cfg_attr(windows, ignore)] fn hello_world() { test_roc_app_slim( - "examples", + "crates/cli/tests/cli", "helloWorld.roc", "Hello, World!\n", UseValgrind::Yes, @@ -493,11 +501,11 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] // uses C platform - fn platform_switching_main() { + fn platform_switching_c() { test_roc_app_slim( - "examples/platform-switching", - "main.roc", - &("Which platform am I running on now?".to_string() + LINE_ENDING), + "examples/platform-switching/c", + "rocLovesC.roc", + &("Roc <3 C!".to_string() + LINE_ENDING), UseValgrind::Yes, ) } @@ -510,7 +518,7 @@ mod cli_run { #[cfg_attr(windows, ignore)] fn platform_switching_rust() { test_roc_app_slim( - "examples/platform-switching", + "examples/platform-switching/rust", "rocLovesRust.roc", "Roc <3 Rust!\n", UseValgrind::Yes, @@ -523,7 +531,7 @@ mod cli_run { #[cfg_attr(windows, ignore)] fn platform_switching_zig() { test_roc_app_slim( - "examples/platform-switching", + "examples/platform-switching/zig", "rocLovesZig.roc", "Roc <3 Zig!\n", UseValgrind::Yes, @@ -543,7 +551,7 @@ mod cli_run { #[test] fn platform_switching_swift() { test_roc_app_slim( - "examples/platform-switching", + "examples/platform-switching/swift", "rocLovesSwift.roc", "Roc <3 Swift!\n", UseValgrind::Yes, @@ -683,6 +691,7 @@ mod cli_run { ); } + #[ignore = "TODO - review or restore this test, write a script that builds the platform files"] #[test] #[cfg_attr( windows, @@ -704,8 +713,8 @@ mod cli_run { )] fn fibonacci() { test_roc_app_slim( - "crates/cli/tests/algorithms", - "fibonacci.roc", + "crates/cli/tests/algorithms/fibonacci/", + "app.roc", "", UseValgrind::Yes, ) @@ -713,15 +722,15 @@ mod cli_run { #[test] fn hello_gui() { - test_roc_app_slim("examples/gui", "hello-guiBROKEN.roc", "", UseValgrind::No) + test_roc_app_slim("examples/gui", "hello-gui.roc", "", UseValgrind::No) } #[test] #[cfg_attr(windows, ignore)] fn quicksort() { test_roc_app_slim( - "crates/cli/tests/algorithms", - "quicksort.roc", + "crates/cli/tests/algorithms/quicksort", + "app.roc", "[0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2]\n", UseValgrind::Yes, ) @@ -733,7 +742,7 @@ mod cli_run { #[serial(cli_platform)] fn cli_args() { test_roc_app( - "examples/cli", + "crates/cli/tests/cli", "argsBROKEN.roc", &[], &[ @@ -826,9 +835,13 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] fn interactive_effects() { + rebuild_host( + &dir_path_from_root("crates/cli/tests/effects"), + &file_path_from_root("crates/cli/tests/effects", "app.roc"), + ); test_roc_app( - "examples/cli", - "effects.roc", + "crates/cli/tests/effects", + "app.roc", &["hi there!"], &[], &[], @@ -843,8 +856,8 @@ mod cli_run { // tea = The Elm Architecture fn terminal_ui_tea() { test_roc_app( - "examples/cli", - "tui.roc", + "crates/cli/tests/tui", + "app.roc", &["foo\n"], // NOTE: adding more lines leads to memory leaks &[], &[], @@ -857,11 +870,13 @@ mod cli_run { #[test] #[cfg_attr(any(target_os = "windows", target_os = "linux"), ignore = "Segfault")] fn false_interpreter() { + let arg = file_path_from_root("crates/cli/tests/false-interpreter/examples", "sqrt.false"); + test_roc_app( - "examples/cli/false-interpreter", - "False.roc", + "crates/cli/tests/false-interpreter/", + "app.roc", &[], - &[Arg::ExamplePath("examples/sqrt.false")], + &[Arg::ExamplePath(arg.display().to_string().as_str())], &[], "1414", UseValgrind::Yes, @@ -869,6 +884,7 @@ mod cli_run { ) } + #[ignore = "TODO - review or restore this test, write a script that builds the platform files"] #[test] fn swift_ui() { test_roc_app_slim("examples/swiftui", "main.roc", "", UseValgrind::No) @@ -983,6 +999,7 @@ mod cli_run { ) } + #[ignore = "TODO restore this test - needs to have a build.roc platform files"] #[test] fn inspect_gui() { test_roc_app_slim("examples", "inspect-gui.roc", "", UseValgrind::No) @@ -991,24 +1008,28 @@ mod cli_run { // TODO not sure if this cfg should still be here: #[cfg(not(debug_assertions))] // this is for testing the benchmarks, to perform proper benchmarks see crates/cli/benches/README.md mod test_benchmarks { + #[allow(unused_imports)] use super::{TestCliCommands, UseValgrind}; use cli_utils::helpers::cli_testing_dir; #[allow(unused_imports)] - use super::{check_output_with_stdin, OPTIMIZE_FLAG, PREBUILT_PLATFORM}; + use super::{check_output_with_stdin, OPTIMIZE_FLAG}; #[allow(unused_imports)] use std::{path::Path, sync::Once}; fn test_benchmark( roc_filename: &str, + dir_name: &std::path::PathBuf, stdin: &[&str], expected_ending: &str, _use_valgrind: UseValgrind, ) { let file_name = cli_testing_dir("benchmarks").join(roc_filename); + cli_utils::helpers::rebuild_host(&cli_testing_dir("benchmarks"), &file_name); + // TODO fix QuicksortApp and then remove this! match roc_filename { "quicksortApp.roc" => { @@ -1029,7 +1050,7 @@ mod cli_run { } #[cfg(all(not(feature = "wasm32-cli-run"), not(feature = "i386-cli-run")))] - check_output_regular(&file_name, stdin, expected_ending, _use_valgrind); + check_output_regular(&file_name, dir_name, stdin, expected_ending, _use_valgrind); #[cfg(feature = "wasm32-cli-run")] check_output_wasm(&file_name, stdin, expected_ending); @@ -1044,6 +1065,7 @@ mod cli_run { #[cfg(all(not(feature = "wasm32-cli-run"), not(feature = "i386-cli-run")))] fn check_output_regular( file_name: &Path, + dir_name: &std::path::PathBuf, stdin: &[&str], expected_ending: &str, use_valgrind: UseValgrind, @@ -1054,6 +1076,7 @@ mod cli_run { // Check with and without optimizations check_output_with_stdin( file_name, + dir_name, stdin, &[], &[], @@ -1073,8 +1096,9 @@ mod cli_run { // Check with and without optimizations check_output_with_stdin( file_name, + dir_name, stdin, - &[PREBUILT_PLATFORM], + &[], &[], &[], expected_ending, @@ -1085,8 +1109,9 @@ mod cli_run { check_output_with_stdin( file_name, + dir_name, stdin, - &[PREBUILT_PLATFORM, OPTIMIZE_FLAG], + &[OPTIMIZE_FLAG], &[], &[], expected_ending, @@ -1175,13 +1200,25 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] fn nqueens() { - test_benchmark("nQueens.roc", &["6"], "4\n", UseValgrind::Yes) + test_benchmark( + "nQueens.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &["6"], + "4\n", + UseValgrind::Yes, + ) } #[test] #[cfg_attr(windows, ignore)] fn cfold() { - test_benchmark("cFold.roc", &["3"], "11 & 11\n", UseValgrind::Yes) + test_benchmark( + "cFold.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &["3"], + "11 & 11\n", + UseValgrind::Yes, + ) } #[test] @@ -1189,6 +1226,7 @@ mod cli_run { fn deriv() { test_benchmark( "deriv.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), &["2"], "1 count: 6\n2 count: 22\n", UseValgrind::Yes, @@ -1198,7 +1236,13 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] fn rbtree_ck() { - test_benchmark("rBTreeCk.roc", &["100"], "10\n", UseValgrind::Yes) + test_benchmark( + "rBTreeCk.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &["100"], + "10\n", + UseValgrind::Yes, + ) } #[test] @@ -1206,6 +1250,7 @@ mod cli_run { fn rbtree_insert() { test_benchmark( "rBTreeInsert.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), &[], "Node Black 0 {} Empty Empty\n", UseValgrind::Yes, @@ -1228,7 +1273,13 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] fn astar() { - test_benchmark("testAStar.roc", &[], "True\n", UseValgrind::No) + test_benchmark( + "testAStar.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &[], + "True\n", + UseValgrind::No, + ) } #[test] @@ -1236,6 +1287,7 @@ mod cli_run { fn base64() { test_benchmark( "testBase64.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), &[], "encoded: SGVsbG8gV29ybGQ=\ndecoded: Hello World\n", UseValgrind::Yes, @@ -1245,19 +1297,32 @@ mod cli_run { #[test] #[cfg_attr(windows, ignore)] fn closure() { - test_benchmark("closure.roc", &[], "", UseValgrind::No) + test_benchmark( + "closure.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &[], + "", + UseValgrind::No, + ) } #[test] #[cfg_attr(windows, ignore)] fn issue2279() { - test_benchmark("issue2279.roc", &[], "Hello, world!\n", UseValgrind::Yes) + test_benchmark( + "issue2279.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), + &[], + "Hello, world!\n", + UseValgrind::Yes, + ) } #[test] fn quicksort_app() { test_benchmark( "quicksortApp.roc", + &cli_utils::helpers::cli_testing_dir("benchmarks"), &[], "todo put the correct quicksort answer here", UseValgrind::Yes, @@ -1271,6 +1336,7 @@ mod cli_run { fn run_multi_dep_str_unoptimized() { check_output_with_stdin( &fixture_file("multi-dep-str", "Main.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("multi-dep-str"), &[], &[], &[], @@ -1287,6 +1353,7 @@ mod cli_run { fn run_multi_dep_str_optimized() { check_output_with_stdin( &fixture_file("multi-dep-str", "Main.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("multi-dep-str"), &[], &[OPTIMIZE_FLAG], &[], @@ -1303,6 +1370,7 @@ mod cli_run { fn run_multi_dep_thunk_unoptimized() { check_output_with_stdin( &fixture_file("multi-dep-thunk", "Main.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("multi-dep-thunk"), &[], &[], &[], @@ -1322,6 +1390,7 @@ mod cli_run { fn run_multi_dep_thunk_optimized() { check_output_with_stdin( &fixture_file("multi-dep-thunk", "Main.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("multi-dep-thunk"), &[], &[OPTIMIZE_FLAG], &[], @@ -1338,6 +1407,7 @@ mod cli_run { fn run_packages_unoptimized() { check_output_with_stdin( &fixture_file("packages", "app.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("packages"), &[], &[], &[], @@ -1354,6 +1424,7 @@ mod cli_run { fn run_packages_optimized() { check_output_with_stdin( &fixture_file("packages", "app.roc"), + &cli_utils::helpers::cli_testing_dir("fixtures").join("packages"), &[], &[OPTIMIZE_FLAG], &[], @@ -1371,31 +1442,50 @@ mod cli_run { &[], indoc!( r#" - ── TYPE MISMATCH in tests/known_bad/TypeError.roc ────────────────────────────── - - Something is off with the body of the main definition: - - 6│ main : Str -> Task {} [] - 7│ main = /_ -> - 8│ "this is a string, not a Task {} [] function like the platform expects." - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - - The body is a string of type: - - Str - + ── TYPE MISMATCH in ....0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY/main.roc ─ + + Something is off with the type annotation of the main required symbol: + + 2│ requires {} { main : Task {} [Exit I32 Str]_ } + ^^^^^^^^^^^^^^^^^^^^^^^ + + This #UserApp.main value is a: + + (* -> Str) + But the type annotation on main says it should be: - - Effect.Effect (Result {} []) - + + InternalTask.Task {} [Exit I32 Str] + Tip: Type comparisons between an opaque type are only ever equal if both types are the same opaque type. Did you mean to create an opaque type by wrapping it? If I have an opaque type Age := U32 I can create an instance of this opaque type by doing @Age 23. - + + + ── TYPE MISMATCH in ....0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY/main.roc ─ + + This 1st argument to attempt has an unexpected type: + + 28│ Task.attempt main /res -> + ^^^^ + + This #UserApp.main value is a: + + (* -> Str) + + But attempt needs its 1st argument to be: + + InternalTask.Task a b + + Tip: Type comparisons between an opaque type are only ever equal if + both types are the same opaque type. Did you mean to create an opaque + type by wrapping it? If I have an opaque type Age := U32 I can create + an instance of this opaque type by doing @Age 23. + ──────────────────────────────────────────────────────────────────────────────── - - 1 error and 0 warnings found in ms."# + + 2 errors and 0 warnings found in ms."# ), ); } diff --git a/crates/cli/tests/effects/.gitignore b/crates/cli/tests/effects/.gitignore new file mode 100644 index 00000000000..f6dd3089d9c --- /dev/null +++ b/crates/cli/tests/effects/.gitignore @@ -0,0 +1,7 @@ +zig-cache/ +zig-out/ +glue/ + +build + +*.a \ No newline at end of file diff --git a/examples/cli/effects-platform/Effect.roc b/crates/cli/tests/effects/Effect.roc similarity index 100% rename from examples/cli/effects-platform/Effect.roc rename to crates/cli/tests/effects/Effect.roc diff --git a/examples/cli/effects.roc b/crates/cli/tests/effects/app.roc similarity index 78% rename from examples/cli/effects.roc rename to crates/cli/tests/effects/app.roc index 37685cb4305..ff93c488a35 100644 --- a/examples/cli/effects.roc +++ b/crates/cli/tests/effects/app.roc @@ -1,6 +1,7 @@ -app [main] { pf: platform "effects-platform/main.roc" } - -import pf.Effect +app "effects" + packages { pf: "platform.roc" } + imports [pf.Effect] + provides [main] to pf main : Effect.Effect {} main = diff --git a/crates/cli/tests/effects/build.roc b/crates/cli/tests/effects/build.roc new file mode 100644 index 00000000000..81d3db93a20 --- /dev/null +++ b/crates/cli/tests/effects/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = Help.prebuiltBinaryName target + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "glue/", "platform.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libfibonacci.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/effects/build.zig b/crates/cli/tests/effects/build.zig new file mode 100644 index 00000000000..1896f1bfa73 --- /dev/null +++ b/crates/cli/tests/effects/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "fibonacci", + .root_source_file = .{ .path = "host.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/examples/cli/effects-platform/host.zig b/crates/cli/tests/effects/host.zig similarity index 99% rename from examples/cli/effects-platform/host.zig rename to crates/cli/tests/effects/host.zig index 04b650705f8..a7d02cbe7aa 100644 --- a/examples/cli/effects-platform/host.zig +++ b/crates/cli/tests/effects/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/examples/cli/effects-platform/main.roc b/crates/cli/tests/effects/platform.roc similarity index 100% rename from examples/cli/effects-platform/main.roc rename to crates/cli/tests/effects/platform.roc diff --git a/crates/cli/tests/expects/.gitignore b/crates/cli/tests/expects/.gitignore new file mode 100644 index 00000000000..195cf6cd71d --- /dev/null +++ b/crates/cli/tests/expects/.gitignore @@ -0,0 +1,6 @@ +zig-out/ +zig-cache/ +build +host/glue/ +expects +platform/*.a \ No newline at end of file diff --git a/crates/cli/tests/expects/build.roc b/crates/cli/tests/expects/build.roc new file mode 100644 index 00000000000..ad69256a094 --- /dev/null +++ b/crates/cli/tests/expects/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/expects/build.zig b/crates/cli/tests/expects/build.zig new file mode 100644 index 00000000000..93a3afde53e --- /dev/null +++ b/crates/cli/tests/expects/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/expects/expects.roc b/crates/cli/tests/expects/expects.roc index e2125c0d99e..68a161821fb 100644 --- a/crates/cli/tests/expects/expects.roc +++ b/crates/cli/tests/expects/expects.roc @@ -1,5 +1,5 @@ app "expects-test" - packages { pf: "zig-platform/main.roc" } + packages { pf: "platform/main.roc" } imports [] provides [main] to pf diff --git a/crates/cli/tests/expects/zig-platform/host.zig b/crates/cli/tests/expects/host/main.zig similarity index 99% rename from crates/cli/tests/expects/zig-platform/host.zig rename to crates/cli/tests/expects/host/main.zig index 1ac9ce6dbed..904ae5a18e8 100644 --- a/crates/cli/tests/expects/zig-platform/host.zig +++ b/crates/cli/tests/expects/host/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/expects/zig-platform/main.roc b/crates/cli/tests/expects/platform/main.roc similarity index 100% rename from crates/cli/tests/expects/zig-platform/main.roc rename to crates/cli/tests/expects/platform/main.roc diff --git a/crates/cli/tests/false-interpreter/.gitignore b/crates/cli/tests/false-interpreter/.gitignore new file mode 100644 index 00000000000..d60a423d39e --- /dev/null +++ b/crates/cli/tests/false-interpreter/.gitignore @@ -0,0 +1,4 @@ +target/ +platform/*.a +build +app \ No newline at end of file diff --git a/examples/cli/false-interpreter/platform/Cargo.toml b/crates/cli/tests/false-interpreter/Cargo.toml similarity index 76% rename from examples/cli/false-interpreter/platform/Cargo.toml rename to crates/cli/tests/false-interpreter/Cargo.toml index 96421c2fcf1..80739155226 100644 --- a/examples/cli/false-interpreter/platform/Cargo.toml +++ b/crates/cli/tests/false-interpreter/Cargo.toml @@ -5,17 +5,11 @@ edition = "2021" license = "UPL-1.0" version = "0.0.1" -links = "app" - [lib] name = "host" -path = "src/lib.rs" +path = "host/lib.rs" crate-type = ["staticlib", "lib"] -[[bin]] -name = "host" -path = "src/main.rs" - [dependencies] libc = "0.2" roc_std = { path = "../../../../crates/roc_std" } diff --git a/examples/cli/false-interpreter/README.md b/crates/cli/tests/false-interpreter/README.md similarity index 100% rename from examples/cli/false-interpreter/README.md rename to crates/cli/tests/false-interpreter/README.md diff --git a/examples/cli/false-interpreter/False.roc b/crates/cli/tests/false-interpreter/app.roc similarity index 99% rename from examples/cli/false-interpreter/False.roc rename to crates/cli/tests/false-interpreter/app.roc index 4c59516c1db..02506fb0805 100644 --- a/examples/cli/false-interpreter/False.roc +++ b/crates/cli/tests/false-interpreter/app.roc @@ -1,10 +1,7 @@ -app [main] { pf: platform "platform/main.roc" } - -import pf.Task exposing [Task] -import pf.Stdout -import pf.Stdin -import Context exposing [Context] -import Variable exposing [Variable] +app "false" + packages { pf: "platform/main.roc" } + imports [pf.Task.{ Task }, pf.Stdout, pf.Stdin, pf.Context.{ Context }, pf.Variable.{ Variable }] + provides [main] to pf # An interpreter for the False programming language: https://strlen.com/false-language/ # This is just a silly example to test this variety of program. diff --git a/crates/cli/tests/false-interpreter/build.roc b/crates/cli/tests/false-interpreter/build.roc new file mode 100644 index 00000000000..66fe0bc4749 --- /dev/null +++ b/crates/cli/tests/false-interpreter/build.roc @@ -0,0 +1,50 @@ +app "build-quicksort" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + + # build the host + Cmd.exec "cargo" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "target/debug/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/cli/false-interpreter/examples/bottles.false b/crates/cli/tests/false-interpreter/examples/bottles.false similarity index 100% rename from examples/cli/false-interpreter/examples/bottles.false rename to crates/cli/tests/false-interpreter/examples/bottles.false diff --git a/examples/cli/false-interpreter/examples/cksum.false b/crates/cli/tests/false-interpreter/examples/cksum.false similarity index 100% rename from examples/cli/false-interpreter/examples/cksum.false rename to crates/cli/tests/false-interpreter/examples/cksum.false diff --git a/examples/cli/false-interpreter/examples/copy.false b/crates/cli/tests/false-interpreter/examples/copy.false similarity index 100% rename from examples/cli/false-interpreter/examples/copy.false rename to crates/cli/tests/false-interpreter/examples/copy.false diff --git a/examples/cli/false-interpreter/examples/crc32.false b/crates/cli/tests/false-interpreter/examples/crc32.false similarity index 100% rename from examples/cli/false-interpreter/examples/crc32.false rename to crates/cli/tests/false-interpreter/examples/crc32.false diff --git a/examples/cli/false-interpreter/examples/hello.false b/crates/cli/tests/false-interpreter/examples/hello.false similarity index 100% rename from examples/cli/false-interpreter/examples/hello.false rename to crates/cli/tests/false-interpreter/examples/hello.false diff --git a/examples/cli/false-interpreter/examples/in.txt b/crates/cli/tests/false-interpreter/examples/in.txt similarity index 100% rename from examples/cli/false-interpreter/examples/in.txt rename to crates/cli/tests/false-interpreter/examples/in.txt diff --git a/examples/cli/false-interpreter/examples/odd_words.false b/crates/cli/tests/false-interpreter/examples/odd_words.false similarity index 100% rename from examples/cli/false-interpreter/examples/odd_words.false rename to crates/cli/tests/false-interpreter/examples/odd_words.false diff --git a/examples/cli/false-interpreter/examples/primes.false b/crates/cli/tests/false-interpreter/examples/primes.false similarity index 100% rename from examples/cli/false-interpreter/examples/primes.false rename to crates/cli/tests/false-interpreter/examples/primes.false diff --git a/examples/cli/false-interpreter/examples/queens.false b/crates/cli/tests/false-interpreter/examples/queens.false similarity index 100% rename from examples/cli/false-interpreter/examples/queens.false rename to crates/cli/tests/false-interpreter/examples/queens.false diff --git a/examples/cli/false-interpreter/examples/sqrt.false b/crates/cli/tests/false-interpreter/examples/sqrt.false similarity index 100% rename from examples/cli/false-interpreter/examples/sqrt.false rename to crates/cli/tests/false-interpreter/examples/sqrt.false diff --git a/examples/cli/false-interpreter/examples/test.false b/crates/cli/tests/false-interpreter/examples/test.false similarity index 100% rename from examples/cli/false-interpreter/examples/test.false rename to crates/cli/tests/false-interpreter/examples/test.false diff --git a/examples/cli/false-interpreter/platform/src/lib.rs b/crates/cli/tests/false-interpreter/host/lib.rs similarity index 98% rename from examples/cli/false-interpreter/platform/src/lib.rs rename to crates/cli/tests/false-interpreter/host/lib.rs index b889ee3678c..b320831eb8d 100644 --- a/examples/cli/false-interpreter/platform/src/lib.rs +++ b/crates/cli/tests/false-interpreter/host/lib.rs @@ -5,10 +5,8 @@ use core::mem::MaybeUninit; use libc; use roc_std::{RocList, RocStr}; use std::env; -use std::ffi::CStr; use std::fs::File; use std::io::{BufRead, BufReader, Read, Write}; -use std::os::raw::c_char; extern "C" { #[link_name = "roc__mainForHost_1_exposed_generic"] @@ -102,7 +100,7 @@ pub unsafe extern "C" fn roc_shm_open( } #[no_mangle] -pub extern "C" fn rust_main() -> i32 { +pub extern "C" fn main() { let arg = env::args() .nth(1) .expect("Please pass a .false file as a command-line argument to the false interpreter!"); @@ -126,8 +124,7 @@ pub extern "C" fn rust_main() -> i32 { result }; - // Exit code - 0 + std::process::exit(0); } unsafe fn call_the_closure(closure_data_ptr: *const u8) -> i64 { diff --git a/examples/cli/false-interpreter/Context.roc b/crates/cli/tests/false-interpreter/platform/Context.roc similarity index 95% rename from examples/cli/false-interpreter/Context.roc rename to crates/cli/tests/false-interpreter/platform/Context.roc index 747124a3342..4ced81e0774 100644 --- a/examples/cli/false-interpreter/Context.roc +++ b/crates/cli/tests/false-interpreter/platform/Context.roc @@ -1,8 +1,6 @@ -module [Context, Data, with, getChar, Option, pushStack, popStack, toStr, inWhileScope] - -import pf.File -import pf.Task exposing [Task] -import Variable exposing [Variable] +interface Context + exposes [Context, Data, with, getChar, Option, pushStack, popStack, toStr, inWhileScope] + imports [File, Task.{ Task }, Variable.{ Variable }] Option a : [Some a, None] diff --git a/examples/cli/false-interpreter/platform/Effect.roc b/crates/cli/tests/false-interpreter/platform/Effect.roc similarity index 100% rename from examples/cli/false-interpreter/platform/Effect.roc rename to crates/cli/tests/false-interpreter/platform/Effect.roc diff --git a/examples/cli/false-interpreter/platform/File.roc b/crates/cli/tests/false-interpreter/platform/File.roc similarity index 88% rename from examples/cli/false-interpreter/platform/File.roc rename to crates/cli/tests/false-interpreter/platform/File.roc index 9d6e2e86772..e6cf96ea038 100644 --- a/examples/cli/false-interpreter/platform/File.roc +++ b/crates/cli/tests/false-interpreter/platform/File.roc @@ -1,7 +1,6 @@ -module [line, Handle, withOpen, chunk] - -import pf.Effect -import Task exposing [Task] +interface File + exposes [line, Handle, withOpen, chunk] + imports [Effect, Task.{ Task }] Handle := U64 diff --git a/examples/cli/false-interpreter/platform/Stdin.roc b/crates/cli/tests/false-interpreter/platform/Stdin.roc similarity index 78% rename from examples/cli/false-interpreter/platform/Stdin.roc rename to crates/cli/tests/false-interpreter/platform/Stdin.roc index 0f03c1f9464..d68be042bc5 100644 --- a/examples/cli/false-interpreter/platform/Stdin.roc +++ b/crates/cli/tests/false-interpreter/platform/Stdin.roc @@ -1,7 +1,6 @@ -module [char] - -import pf.Effect -import Task +interface Stdin + exposes [char] + imports [Effect, Task] # line : Task.Task Str * # line = Effect.after Effect.getLine Task.succeed # TODO FIXME Effect.getLine should suffice diff --git a/examples/cli/false-interpreter/platform/Stdout.roc b/crates/cli/tests/false-interpreter/platform/Stdout.roc similarity index 68% rename from examples/cli/false-interpreter/platform/Stdout.roc rename to crates/cli/tests/false-interpreter/platform/Stdout.roc index 54b45834f51..35a0f24ea27 100644 --- a/examples/cli/false-interpreter/platform/Stdout.roc +++ b/crates/cli/tests/false-interpreter/platform/Stdout.roc @@ -1,7 +1,6 @@ -module [line, raw] - -import pf.Effect -import Task exposing [Task] +interface Stdout + exposes [line, raw] + imports [Effect, Task.{ Task }] line : Str -> Task {} * line = \str -> Effect.map (Effect.putLine str) (\_ -> Ok {}) diff --git a/examples/cli/false-interpreter/platform/Task.roc b/crates/cli/tests/false-interpreter/platform/Task.roc similarity index 93% rename from examples/cli/false-interpreter/platform/Task.roc rename to crates/cli/tests/false-interpreter/platform/Task.roc index d5f212da476..f4870ad46af 100644 --- a/examples/cli/false-interpreter/platform/Task.roc +++ b/crates/cli/tests/false-interpreter/platform/Task.roc @@ -1,6 +1,6 @@ -module [Task, succeed, fail, await, map, onFail, attempt, fromResult, loop] - -import pf.Effect +interface Task + exposes [Task, succeed, fail, await, map, onFail, attempt, fromResult, loop] + imports [Effect] Task ok err : Effect.Effect (Result ok err) diff --git a/examples/cli/false-interpreter/Variable.roc b/crates/cli/tests/false-interpreter/platform/Variable.roc similarity index 100% rename from examples/cli/false-interpreter/Variable.roc rename to crates/cli/tests/false-interpreter/platform/Variable.roc diff --git a/examples/cli/false-interpreter/platform/main.roc b/crates/cli/tests/false-interpreter/platform/main.roc similarity index 86% rename from examples/cli/false-interpreter/platform/main.roc rename to crates/cli/tests/false-interpreter/platform/main.roc index cf97352ce43..e090cc39959 100644 --- a/examples/cli/false-interpreter/platform/main.roc +++ b/crates/cli/tests/false-interpreter/platform/main.roc @@ -1,6 +1,6 @@ platform "false-interpreter" requires {} { main : Str -> Task {} [] } - exposes [] + exposes [Context, Variable] packages {} imports [Task.{ Task }] provides [mainForHost] diff --git a/crates/cli/tests/fixtures/multi-dep-str/.gitignore b/crates/cli/tests/fixtures/multi-dep-str/.gitignore new file mode 100644 index 00000000000..195cf6cd71d --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/.gitignore @@ -0,0 +1,6 @@ +zig-out/ +zig-cache/ +build +host/glue/ +expects +platform/*.a \ No newline at end of file diff --git a/crates/cli/tests/fixtures/multi-dep-str/build.roc b/crates/cli/tests/fixtures/multi-dep-str/build.roc new file mode 100644 index 00000000000..06eedb571cc --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/fixtures/multi-dep-str/build.zig b/crates/cli/tests/fixtures/multi-dep-str/build.zig new file mode 100644 index 00000000000..93a3afde53e --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-str/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/fixtures/multi-dep-str/platform/host.zig b/crates/cli/tests/fixtures/multi-dep-str/host/main.zig similarity index 99% rename from crates/cli/tests/fixtures/multi-dep-str/platform/host.zig rename to crates/cli/tests/fixtures/multi-dep-str/host/main.zig index 029a3ea4ed8..dcb7480c6b2 100644 --- a/crates/cli/tests/fixtures/multi-dep-str/platform/host.zig +++ b/crates/cli/tests/fixtures/multi-dep-str/host/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/fixtures/multi-dep-str/platform/main.roc b/crates/cli/tests/fixtures/multi-dep-str/platform/main.roc index edc3368f937..a52fe9a4801 100644 --- a/crates/cli/tests/fixtures/multi-dep-str/platform/main.roc +++ b/crates/cli/tests/fixtures/multi-dep-str/platform/main.roc @@ -1,5 +1,5 @@ -platform "multi-module" - requires {}{ main : Str } +platform "echo-in-zig" + requires {} { main : Str } exposes [] packages {} imports [] diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore b/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore new file mode 100644 index 00000000000..195cf6cd71d --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/.gitignore @@ -0,0 +1,6 @@ +zig-out/ +zig-cache/ +build +host/glue/ +expects +platform/*.a \ No newline at end of file diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/build.roc b/crates/cli/tests/fixtures/multi-dep-thunk/build.roc new file mode 100644 index 00000000000..06eedb571cc --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/build.zig b/crates/cli/tests/fixtures/multi-dep-thunk/build.zig new file mode 100644 index 00000000000..93a3afde53e --- /dev/null +++ b/crates/cli/tests/fixtures/multi-dep-thunk/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig b/crates/cli/tests/fixtures/multi-dep-thunk/host/main.zig similarity index 99% rename from crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig rename to crates/cli/tests/fixtures/multi-dep-thunk/host/main.zig index 3859fd90154..11a21de8a7d 100644 --- a/crates/cli/tests/fixtures/multi-dep-thunk/platform/host.zig +++ b/crates/cli/tests/fixtures/multi-dep-thunk/host/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc b/crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc index ad427260fa2..a52fe9a4801 100644 --- a/crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc +++ b/crates/cli/tests/fixtures/multi-dep-thunk/platform/main.roc @@ -1,5 +1,5 @@ -platform "multi-dep-thunk" - requires {}{ main : Str } +platform "echo-in-zig" + requires {} { main : Str } exposes [] packages {} imports [] diff --git a/crates/cli/tests/fixtures/packages/.gitignore b/crates/cli/tests/fixtures/packages/.gitignore new file mode 100644 index 00000000000..195cf6cd71d --- /dev/null +++ b/crates/cli/tests/fixtures/packages/.gitignore @@ -0,0 +1,6 @@ +zig-out/ +zig-cache/ +build +host/glue/ +expects +platform/*.a \ No newline at end of file diff --git a/crates/cli/tests/fixtures/packages/build.roc b/crates/cli/tests/fixtures/packages/build.roc new file mode 100644 index 00000000000..06eedb571cc --- /dev/null +++ b/crates/cli/tests/fixtures/packages/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/fixtures/packages/build.zig b/crates/cli/tests/fixtures/packages/build.zig new file mode 100644 index 00000000000..93a3afde53e --- /dev/null +++ b/crates/cli/tests/fixtures/packages/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/crates/cli/tests/fixtures/packages/platform/host.zig b/crates/cli/tests/fixtures/packages/host/main.zig similarity index 99% rename from crates/cli/tests/fixtures/packages/platform/host.zig rename to crates/cli/tests/fixtures/packages/host/main.zig index 029a3ea4ed8..dcb7480c6b2 100644 --- a/crates/cli/tests/fixtures/packages/platform/host.zig +++ b/crates/cli/tests/fixtures/packages/host/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/fixtures/packages/platform/main.roc b/crates/cli/tests/fixtures/packages/platform/main.roc index edc3368f937..a52fe9a4801 100644 --- a/crates/cli/tests/fixtures/packages/platform/main.roc +++ b/crates/cli/tests/fixtures/packages/platform/main.roc @@ -1,5 +1,5 @@ -platform "multi-module" - requires {}{ main : Str } +platform "echo-in-zig" + requires {} { main : Str } exposes [] packages {} imports [] diff --git a/crates/cli/tests/known_bad/TypeError.roc b/crates/cli/tests/known_bad/TypeError.roc index 070aca541ae..7d5ae09b117 100644 --- a/crates/cli/tests/known_bad/TypeError.roc +++ b/crates/cli/tests/known_bad/TypeError.roc @@ -1,8 +1,7 @@ app "type-error" - packages { pf: "../../../../examples/cli/false-interpreter/platform/main.roc" } + packages { pf: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br" } imports [pf.Task.{ Task }] provides [main] to pf -main : Str -> Task {} [] main = \_ -> "this is a string, not a Task {} [] function like the platform expects." \ No newline at end of file diff --git a/crates/cli/tests/tui/.gitignore b/crates/cli/tests/tui/.gitignore new file mode 100644 index 00000000000..42617dbe979 --- /dev/null +++ b/crates/cli/tests/tui/.gitignore @@ -0,0 +1,5 @@ +glue/ +zig-cache/ +zig-out/ +build +*.a \ No newline at end of file diff --git a/examples/cli/tui-platform/Program.roc b/crates/cli/tests/tui/Program.roc similarity index 100% rename from examples/cli/tui-platform/Program.roc rename to crates/cli/tests/tui/Program.roc diff --git a/examples/cli/tui.roc b/crates/cli/tests/tui/app.roc similarity index 77% rename from examples/cli/tui.roc rename to crates/cli/tests/tui/app.roc index 4c5f5ed5603..a7554ddd5f3 100644 --- a/examples/cli/tui.roc +++ b/crates/cli/tests/tui/app.roc @@ -1,4 +1,4 @@ -app [main, Model] { pf: platform "tui-platform/main.roc" } +app [main, Model] { pf: platform "platform.roc" } import pf.Program exposing [Program] diff --git a/crates/cli/tests/tui/build.roc b/crates/cli/tests/tui/build.roc new file mode 100644 index 00000000000..6ebfd43a048 --- /dev/null +++ b/crates/cli/tests/tui/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = Help.prebuiltBinaryName target + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "glue/", "platform-glue-workaround.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/crates/cli/tests/tui/build.zig b/crates/cli/tests/tui/build.zig new file mode 100644 index 00000000000..3fe9ddafc42 --- /dev/null +++ b/crates/cli/tests/tui/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/examples/cli/tui-platform/host.zig b/crates/cli/tests/tui/host.zig similarity index 99% rename from examples/cli/tui-platform/host.zig rename to crates/cli/tests/tui/host.zig index a9e9c40f501..48a8a4f90fc 100644 --- a/examples/cli/tui-platform/host.zig +++ b/crates/cli/tests/tui/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/crates/cli/tests/tui/platform-glue-workaround.roc b/crates/cli/tests/tui/platform-glue-workaround.roc new file mode 100644 index 00000000000..c14a4cef216 --- /dev/null +++ b/crates/cli/tests/tui/platform-glue-workaround.roc @@ -0,0 +1,9 @@ +platform "tui" + requires { } { main : _ } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/cli/tui-platform/main.roc b/crates/cli/tests/tui/platform.roc similarity index 100% rename from examples/cli/tui-platform/main.roc rename to crates/cli/tests/tui/platform.roc diff --git a/crates/cli_utils/Cargo.toml b/crates/cli_utils/Cargo.toml index 2530866df57..9182816dbc6 100644 --- a/crates/cli_utils/Cargo.toml +++ b/crates/cli_utils/Cargo.toml @@ -14,6 +14,7 @@ roc_load = { path = "../compiler/load" } roc_module = { path = "../compiler/module" } roc_reporting = { path = "../reporting" } roc_command_utils = { path = "../utils/command" } +roc_error_macros = { path = "../error_macros" } bumpalo.workspace = true criterion.workspace = true diff --git a/crates/cli_utils/src/helpers.rs b/crates/cli_utils/src/helpers.rs index a268d5b0e30..6acb51f9dd9 100644 --- a/crates/cli_utils/src/helpers.rs +++ b/crates/cli_utils/src/helpers.rs @@ -5,6 +5,7 @@ extern crate roc_module; extern crate tempfile; use roc_command_utils::{cargo, pretty_command_string, root_dir}; +use roc_error_macros::internal_error; use serde::Deserialize; use serde_xml_rs::from_str; use std::env; @@ -472,3 +473,38 @@ pub fn known_bad_file(file_name: &str) -> PathBuf { path } + +/// Rebuild the host for a test platform +pub fn rebuild_host(dir_name: &PathBuf, file_name: &PathBuf) { + // find the workspace directory so we can give roc build script absolute paths + let workspace_dir = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap()); + + // check glue spec is available + let zig_glue_path = workspace_dir + .join("..") + .join("glue") + .join("src") + .join("ZigGlue.roc"); + if !zig_glue_path.is_file() { + internal_error!("expected ZigGlue.roc at {}", zig_glue_path.display()); + } + + // check platform folder is available + let platform_path = workspace_dir.join("..").join("..").join(dir_name); + if !platform_path.is_dir() { + internal_error!("expected platform path at {}", platform_path.display()); + } + + // re-build the platform, expect a build.roc to be next to the test file + // set the working directory to the platform folder + let build_script_path = std::path::PathBuf::from(&file_name).with_file_name("build.roc"); + std::process::Command::new("roc") + .current_dir(&platform_path) + .arg(&build_script_path) + .envs(vec![ + ("ROC", "roc"), + ("ZIG_GLUE", zig_glue_path.display().to_string().as_str()), + ]) + .status() + .unwrap_or_else(|_| panic!("unable to run build script {}", build_script_path.display())); +} diff --git a/crates/compiler/build/src/link.rs b/crates/compiler/build/src/link.rs index c1647661d49..afa9683c3ac 100644 --- a/crates/compiler/build/src/link.rs +++ b/crates/compiler/build/src/link.rs @@ -6,12 +6,12 @@ use roc_error_macros::internal_error; use roc_mono::ir::OptLevel; use roc_target::{Architecture, OperatingSystem, Target}; use std::collections::HashMap; +use std::env; use std::ffi::OsString; use std::fs::DirEntry; use std::io; use std::path::{Path, PathBuf}; use std::process::{self, Child, Command}; -use std::{env, fs}; use wasi_libc_sys::{WASI_COMPILER_RT_PATH, WASI_LIBC_PATH}; pub use roc_linker::LinkType; @@ -429,6 +429,7 @@ pub fn build_swift_host_native( command } +/// IMPORTANT: Platforms are responsible for building themselves, this is only used for tests internally pub fn rebuild_host( opt_level: OptLevel, target: Target, @@ -756,7 +757,7 @@ fn find_used_target_sub_folder(opt_level: OptLevel, target_folder: PathBuf) -> P fn find_in_folder_or_subfolders(path: &PathBuf, folder_to_find: &str) -> Vec { let mut matching_dirs = vec![]; - if let Ok(entries) = fs::read_dir(path) { + if let Ok(entries) = std::fs::read_dir(path) { for entry in entries.flatten() { if entry.file_type().unwrap().is_dir() { let dir_name = entry diff --git a/crates/compiler/build/src/program.rs b/crates/compiler/build/src/program.rs index 767ac6c4fc4..f7d93751c14 100644 --- a/crates/compiler/build/src/program.rs +++ b/crates/compiler/build/src/program.rs @@ -1,6 +1,4 @@ -use crate::link::{ - legacy_host_file, link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy, -}; +use crate::link::{legacy_host_file, link, LinkType, LinkingStrategy}; use bumpalo::Bump; use inkwell::memory_buffer::MemoryBuffer; use roc_error_macros::internal_error; @@ -22,7 +20,6 @@ use std::ffi::OsStr; use std::ops::Deref; use std::{ path::{Path, PathBuf}, - thread::JoinHandle, time::{Duration, Instant}, }; @@ -726,7 +723,6 @@ pub fn build_file<'a>( emit_timings: bool, link_type: LinkType, linking_strategy: LinkingStrategy, - prebuilt_requested: bool, wasm_dev_stack_bytes: Option, roc_cache_dir: RocCacheDir<'_>, load_config: LoadConfig, @@ -747,7 +743,6 @@ pub fn build_file<'a>( emit_timings, link_type, linking_strategy, - prebuilt_requested, wasm_dev_stack_bytes, loaded, compilation_start, @@ -764,7 +759,6 @@ fn build_loaded_file<'a>( emit_timings: bool, link_type: LinkType, mut linking_strategy: LinkingStrategy, - prebuilt_requested: bool, wasm_dev_stack_bytes: Option, loaded: roc_load::MonomorphizedModule<'a>, compilation_start: Instant, @@ -775,33 +769,52 @@ fn build_loaded_file<'a>( _ => unreachable!(), }; - // For example, if we're loading the platform from a URL, it's automatically prebuilt - // even if the --prebuilt-platform CLI flag wasn't set. - let is_platform_prebuilt = prebuilt_requested || loaded.uses_prebuilt_platform; - - if is_platform_prebuilt && linking_strategy == LinkingStrategy::Surgical { - // Fallback to legacy linking if the preprocessed host file does not exist, but a legacy host does exist. - let preprocessed_host_path = - platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target)); - let legacy_host_path = legacy_host_file(target, &platform_main_roc); - if !preprocessed_host_path.exists() && legacy_host_path.exists() { - linking_strategy = LinkingStrategy::Legacy; + let preprocessed_host_path = match linking_strategy { + LinkingStrategy::Legacy => { + // if target == Target::Wasm32 { + // // when compiling a wasm application, we implicitly assume here that the host is in zig + // // and has a file called "host.zig" + // platform_main_roc.with_file_name("host.zig") + // } else { + // } + legacy_host_file(target, &platform_main_roc) } - } + LinkingStrategy::Surgical => { + let preprocessed_host_path = + platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target)); + let legacy_host_path = legacy_host_file(target, &platform_main_roc); + + // Fallback to legacy linking if the preprocessed host file does not exist, but a legacy host does exist. + if !preprocessed_host_path.exists() && legacy_host_path.exists() { + linking_strategy = LinkingStrategy::Legacy; + } - // the preprocessed host is stored beside the platform's main.roc - let preprocessed_host_path = if linking_strategy == LinkingStrategy::Legacy { - if target == Target::Wasm32 { - // when compiling a wasm application, we implicitly assume here that the host is in zig - // and has a file called "host.zig" - platform_main_roc.with_file_name("host.zig") - } else { - legacy_host_file(target, &platform_main_roc) + preprocessed_host_path } - } else { - platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target)) + _ => platform_main_roc.with_file_name(roc_linker::preprocessed_host_filename(target)), }; + // for static and dynamic libraries we don't need the prebuilt host + // this is only required for LinkType::Executable + if !preprocessed_host_path.exists() && link_type == LinkType::Executable { + eprintln!( + indoc::indoc!( + r#" + I was expecting to find the following pre-built platform file: + + {} + + However, it was not there! + + If you are using a local platform and not from a URL release, then you should generate this first. + "# + ), + preprocessed_host_path.to_string_lossy(), + ); + + std::process::exit(1); + } + let output_exe_path = match out_path { Some(path) => { // true iff the path ends with a directory separator, @@ -838,46 +851,11 @@ fn build_loaded_file<'a>( None => with_output_extension(&app_module_path, target, linking_strategy, link_type), }; - // We don't need to spawn a rebuild thread when using a prebuilt host. - let rebuild_thread = if matches!(link_type, LinkType::Dylib | LinkType::None) { - None - } else if is_platform_prebuilt { - if !preprocessed_host_path.exists() { - invalid_prebuilt_platform(prebuilt_requested, preprocessed_host_path); - - std::process::exit(1); - } - - if linking_strategy == LinkingStrategy::Surgical { - // Copy preprocessed host to executable location. - // The surgical linker will modify that copy in-place. - std::fs::copy(&preprocessed_host_path, output_exe_path.as_path()).unwrap(); - } - - None - } else { - // TODO this should probably be moved before load_and_monomorphize. - // To do this we will need to preprocess files just for their exported symbols. - // Also, we should no longer need to do this once we have platforms on - // a package repository, as we can then get prebuilt platforms from there. - - let dll_stub_symbols = roc_linker::ExposedSymbols::from_exposed_to_host( - &loaded.interns, - &loaded.exposed_to_host, - ); - - let join_handle = spawn_rebuild_thread( - code_gen_options.opt_level, - linking_strategy, - platform_main_roc.clone(), - preprocessed_host_path.clone(), - output_exe_path.clone(), - target, - dll_stub_symbols, - ); - - Some(join_handle) - }; + if linking_strategy == LinkingStrategy::Surgical { + // Copy preprocessed host to executable location. + // The surgical linker will modify that copy in-place. + std::fs::copy(&preprocessed_host_path, output_exe_path.as_path()).unwrap(); + } let buf = &mut String::with_capacity(1024); @@ -910,29 +888,6 @@ fn build_loaded_file<'a>( let problems = report_problems_monomorphized(&mut loaded); let loaded = loaded; - enum HostRebuildTiming { - BeforeApp(u128), - ConcurrentWithApp(JoinHandle), - } - - let opt_rebuild_timing = if let Some(rebuild_thread) = rebuild_thread { - if linking_strategy == LinkingStrategy::Additive { - let rebuild_duration = rebuild_thread - .join() - .expect("Failed to (re)build platform."); - - if emit_timings && !is_platform_prebuilt { - println!("Finished rebuilding the platform in {rebuild_duration} ms\n"); - } - - Some(HostRebuildTiming::BeforeApp(rebuild_duration)) - } else { - Some(HostRebuildTiming::ConcurrentWithApp(rebuild_thread)) - } - } else { - None - }; - let (roc_app_bytes, code_gen_timing, expect_metadata) = gen_from_mono_module( arena, loaded, @@ -972,14 +927,6 @@ fn build_loaded_file<'a>( ); } - if let Some(HostRebuildTiming::ConcurrentWithApp(thread)) = opt_rebuild_timing { - let rebuild_duration = thread.join().expect("Failed to (re)build platform."); - - if emit_timings && !is_platform_prebuilt { - println!("Finished rebuilding the platform in {rebuild_duration} ms\n"); - } - } - // Step 2: link the prebuilt platform and compiled app let link_start = Instant::now(); @@ -1020,7 +967,6 @@ fn build_loaded_file<'a>( let mut inputs = vec![app_o_file.to_str().unwrap()]; if !matches!(link_type, LinkType::Dylib | LinkType::None) { - // the host has been compiled into a .o or .obj file inputs.push(preprocessed_host_path.as_path().to_str().unwrap()); } @@ -1064,125 +1010,6 @@ fn build_loaded_file<'a>( }) } -fn invalid_prebuilt_platform(prebuilt_requested: bool, preprocessed_host_path: PathBuf) { - let prefix = if prebuilt_requested { - "Because I was run with --prebuilt-platform, " - } else { - "" - }; - - let preprocessed_host_path_str = preprocessed_host_path.to_string_lossy(); - let extra_err_msg = if preprocessed_host_path_str.ends_with(".rh") { - "\n\n\tNote: If the platform does have an .rh1 file but no .rh file, it's because it's been built with an older version of roc. Contact the author to release a new build of the platform using a roc release newer than March 21 2023.\n" - } else { - "" - }; - - eprintln!( - indoc::indoc!( - r#" - {}I was expecting this file to exist: - - {} - - However, it was not there!{} - - If you have the platform's source code locally, you may be able to generate it by re-running this command omitting --prebuilt-platform - "# - ), - prefix, - preprocessed_host_path.to_string_lossy(), - extra_err_msg - ); -} - -#[allow(clippy::too_many_arguments)] -fn spawn_rebuild_thread( - opt_level: OptLevel, - linking_strategy: LinkingStrategy, - platform_main_roc: PathBuf, - preprocessed_host_path: PathBuf, - output_exe_path: PathBuf, - target: Target, - dll_stub_symbols: Vec, -) -> std::thread::JoinHandle { - std::thread::spawn(move || { - // Printing to stderr because we want stdout to contain only the output of the roc program. - // We are aware of the trade-offs. - // `cargo run` follows the same approach - eprintln!("🔨 Rebuilding platform..."); - - let rebuild_host_start = Instant::now(); - - match linking_strategy { - LinkingStrategy::Additive => { - let host_dest = rebuild_host(opt_level, target, platform_main_roc.as_path(), None); - - preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path); - } - LinkingStrategy::Surgical => { - build_and_preprocess_host_lowlevel( - opt_level, - target, - platform_main_roc.as_path(), - preprocessed_host_path.as_path(), - &dll_stub_symbols, - ); - - // Copy preprocessed host to executable location. - // The surgical linker will modify that copy in-place. - std::fs::copy(&preprocessed_host_path, output_exe_path.as_path()).unwrap(); - } - LinkingStrategy::Legacy => { - rebuild_host(opt_level, target, platform_main_roc.as_path(), None); - } - } - - rebuild_host_start.elapsed().as_millis() - }) -} - -pub fn build_and_preprocess_host( - opt_level: OptLevel, - target: Target, - platform_main_roc: &Path, - preprocessed_host_path: &Path, - exposed_symbols: roc_linker::ExposedSymbols, -) { - let stub_dll_symbols = exposed_symbols.stub_dll_symbols(); - - build_and_preprocess_host_lowlevel( - opt_level, - target, - platform_main_roc, - preprocessed_host_path, - &stub_dll_symbols, - ) -} - -fn build_and_preprocess_host_lowlevel( - opt_level: OptLevel, - target: Target, - platform_main_roc: &Path, - preprocessed_host_path: &Path, - stub_dll_symbols: &[String], -) { - let stub_lib = - roc_linker::generate_stub_lib_from_loaded(target, platform_main_roc, stub_dll_symbols); - - debug_assert!(stub_lib.exists()); - - rebuild_host(opt_level, target, platform_main_roc, Some(&stub_lib)); - - roc_linker::preprocess_host( - target, - platform_main_roc, - preprocessed_host_path, - &stub_lib, - stub_dll_symbols, - ) -} - #[allow(clippy::too_many_arguments)] pub fn check_file<'a>( arena: &'a Bump, @@ -1261,7 +1088,6 @@ pub fn build_str_test<'a>( arena: &'a Bump, app_module_path: &Path, app_module_source: &'a str, - assume_prebuild: bool, ) -> Result, BuildFileError<'a>> { let target = target_lexicon::Triple::host().into(); @@ -1305,7 +1131,6 @@ pub fn build_str_test<'a>( emit_timings, link_type, linking_strategy, - assume_prebuild, wasm_dev_stack_bytes, loaded, compilation_start, diff --git a/crates/glue/src/load.rs b/crates/glue/src/load.rs index 3a15704858b..f62a2629af4 100644 --- a/crates/glue/src/load.rs +++ b/crates/glue/src/load.rs @@ -86,7 +86,6 @@ pub fn generate( false, link_type, linking_strategy, - true, None, RocCacheDir::Persistent(cache::roc_cache_dir().as_path()), load_config, diff --git a/crates/linker/src/lib.rs b/crates/linker/src/lib.rs index 7378c8662fe..d671c6ebf41 100644 --- a/crates/linker/src/lib.rs +++ b/crates/linker/src/lib.rs @@ -6,11 +6,8 @@ use memmap2::{Mmap, MmapMut}; use object::Object; use roc_error_macros::internal_error; -use roc_load::{EntryPoint, ExecutionMode, ExposedToHost, LoadConfig, Threading}; +use roc_load::ExposedToHost; use roc_module::symbol::Interns; -use roc_packaging::cache::RocCacheDir; -use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE}; -use roc_solve::FunctionKind; use roc_target::{Architecture, OperatingSystem, Target}; use std::cmp::Ordering; use std::mem; @@ -64,73 +61,6 @@ pub fn link_preprocessed_host( surgery(roc_app_bytes, &metadata, binary_path, false, false, target) } -// Exposed function to load a platform file and generate a stub lib for it. -pub fn generate_stub_lib( - input_path: &Path, - roc_cache_dir: RocCacheDir<'_>, - target: Target, - function_kind: FunctionKind, -) -> (PathBuf, PathBuf, Vec) { - // Note: this should theoretically just be able to load the host, I think. - // Instead, I am loading an entire app because that was simpler and had example code. - // If this was expected to stay around for the the long term, we should change it. - // But hopefully it will be removable once we have surgical linking on all platforms. - let arena = &bumpalo::Bump::new(); - let loaded = roc_load::load_and_monomorphize( - arena, - input_path.to_path_buf(), - roc_cache_dir, - LoadConfig { - target, - function_kind, - render: RenderTarget::Generic, - palette: DEFAULT_PALETTE, - threading: Threading::AllAvailable, - exec_mode: ExecutionMode::Executable, - }, - ) - .unwrap_or_else(|problem| todo!("{:?}", problem)); - - let exposed_to_host = loaded - .exposed_to_host - .top_level_values - .keys() - .map(|x| x.as_str(&loaded.interns).to_string()) - .collect(); - - let exported_closure_types = loaded - .exposed_to_host - .closure_types - .iter() - .map(|x| { - format!( - "{}_{}", - x.module_string(&loaded.interns), - x.as_str(&loaded.interns) - ) - }) - .collect(); - - let exposed_symbols = ExposedSymbols { - top_level_values: exposed_to_host, - exported_closure_types, - }; - - if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point { - let stub_lib = if target.operating_system() == OperatingSystem::Windows { - platform_path.with_file_name("libapp.obj") - } else { - platform_path.with_file_name("libapp.so") - }; - - let stub_dll_symbols = exposed_symbols.stub_dll_symbols(); - generate_dynamic_lib(target, &stub_dll_symbols, &stub_lib); - (platform_path.into(), stub_lib, stub_dll_symbols) - } else { - unreachable!(); - } -} - pub fn generate_stub_lib_from_loaded( target: Target, platform_main_roc: &Path, @@ -364,27 +294,24 @@ fn stub_lib_is_up_to_date(target: Target, stub_lib_path: &Path, custom_names: &[ pub fn preprocess_host( target: Target, - platform_main_roc: &Path, - preprocessed_path: &Path, - shared_lib: &Path, - stub_dll_symbols: &[String], + host_path: &Path, + platform_path: &Path, + dylib_path: &Path, + verbose: bool, + time: bool, ) { - let metadata_path = platform_main_roc.with_file_name(metadata_file_name(target)); - let host_exe_path = if target.operating_system() == OperatingSystem::Windows { - platform_main_roc.with_file_name("dynhost.exe") - } else { - platform_main_roc.with_file_name("dynhost") - }; + let preprocessed_path = platform_path.with_file_name(format!("{}.rh", target)); + let metadata_path = platform_path.with_file_name(metadata_file_name(target)); preprocess( target, - &host_exe_path, + host_path, &metadata_path, - preprocessed_path, - shared_lib, - stub_dll_symbols, - false, - false, + preprocessed_path.as_path(), + dylib_path, + &[], + verbose, + time, ) } diff --git a/crates/linker/src/macho.rs b/crates/linker/src/macho.rs index 2ce2dbced39..05d1bb39878 100644 --- a/crates/linker/src/macho.rs +++ b/crates/linker/src/macho.rs @@ -1088,7 +1088,7 @@ fn gen_macho_le( } } - offset += dbg!(cmd_size); + offset += cmd_size; } // cmd_loc should be where the last offset ended diff --git a/crates/valgrind/src/lib.rs b/crates/valgrind/src/lib.rs index 5bff7e60593..5deb33c7b21 100644 --- a/crates/valgrind/src/lib.rs +++ b/crates/valgrind/src/lib.rs @@ -7,7 +7,6 @@ static BUILD_ONCE: std::sync::Once = std::sync::Once::new(); #[cfg(target_os = "linux")] fn build_host() { - use roc_build::program::build_and_preprocess_host; use roc_linker::preprocessed_host_filename; let platform_main_roc = @@ -26,16 +25,32 @@ fn build_host() { std::env::set_var("NO_AVX512", "1"); } - build_and_preprocess_host( - roc_mono::ir::OptLevel::Normal, - target, - &platform_main_roc, - &preprocessed_host_path, - roc_linker::ExposedSymbols { - top_level_values: vec![String::from("mainForHost")], - exported_closure_types: vec![], - }, - ); + let zig_glue_path = roc_command_utils::root_dir().join("crates/glue/src/ZigGlue.roc"); + let glue_path = roc_command_utils::root_dir().join("crates/valgrind/zig-platform/glue"); + + // generate glue + std::process::Command::new("roc") + .args(&[ + zig_glue_path.display().to_string(), + glue_path.display().to_string(), + platform_main_roc.display().to_string(), + ]) + .status() + .unwrap(); + + let femit_arg = format!("-femit-bin={}", preprocessed_host_path.display()); + let host_path = roc_command_utils::root_dir().join("crates/valgrind/zig-platform/host.zig"); + + // generate host + std::process::Command::new("zig") + .args(&[ + "build-lib", + "-lc", + femit_arg.as_str(), + &host_path.display().to_string(), + ]) + .status() + .unwrap(); } fn valgrind_test(source: &str) { diff --git a/crates/valgrind/zig-platform/.gitignore b/crates/valgrind/zig-platform/.gitignore new file mode 100644 index 00000000000..9a2e491d22e --- /dev/null +++ b/crates/valgrind/zig-platform/.gitignore @@ -0,0 +1,2 @@ +glue +*.a \ No newline at end of file diff --git a/crates/valgrind/zig-platform/host.zig b/crates/valgrind/zig-platform/host.zig index 27df6519182..574dedf62f7 100644 --- a/crates/valgrind/zig-platform/host.zig +++ b/crates/valgrind/zig-platform/host.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/examples/build-helpers/Help.roc b/examples/build-helpers/Help.roc new file mode 100644 index 00000000000..69279b3c08f --- /dev/null +++ b/examples/build-helpers/Help.roc @@ -0,0 +1,76 @@ +interface Help + exposes [ + RocTarget, + Arch, + Os, + archFromStr, + osFromStr, + rocTarget, + prebuiltBinaryName, + dynamicLibraryExtension, + ] + imports [] + + +RocTarget : [ + MacosArm64, + MacosX64, + LinuxArm64, + LinuxX64, + WindowsArm64, + WindowsX64, +] + +Arch : [ + Arm64, + X64, + UnsupportedArch Str, +] + +Os : [ + Macos, + Linux, + UnsupportedOS Str, +] + +archFromStr : List U8 -> Arch +archFromStr = \bytes -> + when Str.fromUtf8 bytes is + Ok str if str == "arm64\n" -> Arm64 + Ok str if str == "x86_64\n" -> X64 + Ok str -> UnsupportedArch str + _ -> crash "invalid utf8 from uname -m" + +osFromStr : List U8 -> Os +osFromStr = \bytes -> + when Str.fromUtf8 bytes is + Ok str if str == "Darwin\n" -> Macos + Ok str if str == "Linux\n" -> Linux + Ok str -> UnsupportedOS str + _ -> crash "invalid utf8 from uname -s" + +rocTarget : {os:Os, arch:Arch} -> Result RocTarget [UnsupportedTarget Os Arch] +rocTarget = \{os, arch} -> + when (os, arch) is + (Macos, Arm64) -> Ok MacosArm64 + (Macos, X64) -> Ok MacosX64 + (Linux, Arm64) -> Ok LinuxArm64 + (Linux, X64) -> Ok LinuxX64 + _ -> Err (UnsupportedTarget os arch) + +prebuiltBinaryName : RocTarget -> Str +prebuiltBinaryName = \target -> + when target is + MacosArm64 -> "macos-arm64.a" + MacosX64 -> "macos-x64" + LinuxArm64 -> "linux-arm64.a" + LinuxX64 -> "linux-x64.a" + WindowsArm64 -> "windows-arm64.a" + WindowsX64 -> "windows-x64" + +dynamicLibraryExtension : RocTarget -> Str +dynamicLibraryExtension = \target -> + when target is + MacosArm64 | MacosX64 -> ".dylib" + LinuxArm64 | LinuxX64 -> ".so" + WindowsArm64 | WindowsX64 -> "windows-arm64.obj" \ No newline at end of file diff --git a/examples/build-helpers/main.roc b/examples/build-helpers/main.roc new file mode 100644 index 00000000000..8888cbd76f1 --- /dev/null +++ b/examples/build-helpers/main.roc @@ -0,0 +1,5 @@ +package "build-helpers" + exposes [ + Help + ] + packages {} diff --git a/examples/cli/README.md b/examples/cli/README.md deleted file mode 100644 index 209a2eb67e2..00000000000 --- a/examples/cli/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# CLI examples - -**Note**: You probably will want to use the [`basic-cli` platform](https://github.com/roc-lang/basic-cli). This folder will be removed soon. - -These are examples of how to make basic CLI (command-line interface) and TUI (terminal user interface) apps in Roc. diff --git a/examples/cli/false-interpreter/platform/build.rs b/examples/cli/false-interpreter/platform/build.rs deleted file mode 100644 index 47763b34c39..00000000000 --- a/examples/cli/false-interpreter/platform/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - #[cfg(not(windows))] - println!("cargo:rustc-link-lib=dylib=app"); - - #[cfg(windows)] - println!("cargo:rustc-link-lib=dylib=libapp"); - - println!("cargo:rustc-link-search=."); -} diff --git a/examples/cli/false-interpreter/platform/host.c b/examples/cli/false-interpreter/platform/host.c deleted file mode 100644 index 0378c69589c..00000000000 --- a/examples/cli/false-interpreter/platform/host.c +++ /dev/null @@ -1,7 +0,0 @@ -#include - -extern int rust_main(); - -int main() { - return rust_main(); -} diff --git a/examples/cli/false-interpreter/platform/src/main.rs b/examples/cli/false-interpreter/platform/src/main.rs deleted file mode 100644 index 0765384f29f..00000000000 --- a/examples/cli/false-interpreter/platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main() as _); -} diff --git a/examples/gui/.gitignore b/examples/gui/.gitignore new file mode 100644 index 00000000000..76ae8ce5ec4 --- /dev/null +++ b/examples/gui/.gitignore @@ -0,0 +1,11 @@ +platform/*.a +platform/*.dylib +platform/*.so +platform/*.rh +platform/*.rm + +target/ + +build + +hello-gui \ No newline at end of file diff --git a/examples/gui/Cargo.toml b/examples/gui/Cargo.toml new file mode 100644 index 00000000000..8731f3ea482 --- /dev/null +++ b/examples/gui/Cargo.toml @@ -0,0 +1,7 @@ +[workspace] +resolver = "2" +members = [ + "roc_host", + "roc_host_lib", +] + diff --git a/examples/gui/build.roc b/examples/gui/build.roc new file mode 100644 index 00000000000..b9335b46aa6 --- /dev/null +++ b/examples/gui/build.roc @@ -0,0 +1,79 @@ +app [main] { + cli: platform "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../examples/build-helpers/main.roc", +} + +import cli.Task exposing [Task] +import cli.Path +import cli.Env +import cli.Cmd +import build.Help + +cargoTargetLibraryPath = "target/debug/libhost.a" +cargoTargetExecutablePath = "target/debug/host" +stubbedDynamicLibraryPath = \target -> "platform/libapp$(Help.dynamicLibraryExtension target)" + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + + Err _ -> + # build the host + Cmd.exec "cargo" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform for legacy linking + Cmd.exec "cp" ["-f", cargoTargetLibraryPath, prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + + # build stubbed dynamic library + Cmd.exec roc [ + "build", + "--lib", + "platform/libapp.roc", + "--output", + stubbedDynamicLibraryPath target, + ] + |> Task.mapErr! ErrBuildingStubbedDylib + + # pre-process host for surgical linking + Cmd.exec roc [ + "preprocess-host", + cargoTargetExecutablePath, + "platform/main.roc", + stubbedDynamicLibraryPath target, + ] + |> Task.mapErr! ErrPreProcessingHost + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/gui/hello-guiBROKEN.roc b/examples/gui/hello-gui.roc similarity index 84% rename from examples/gui/hello-guiBROKEN.roc rename to examples/gui/hello-gui.roc index d5166b4e83d..dc7d66cb8fc 100644 --- a/examples/gui/hello-guiBROKEN.roc +++ b/examples/gui/hello-gui.roc @@ -1,5 +1,4 @@ -app # [pf.Action.{ Action }, pf.Elem.{ button, text, row, col }] - [render] { pf: platform "platform/main.roc" } +app [render] { pf: platform "platform/main.roc" } render = rgba = \r, g, b, a -> { r: r / 255, g: g / 255, b: b / 255, a } diff --git a/examples/gui/platform/build.rs b/examples/gui/platform/build.rs deleted file mode 100644 index 47763b34c39..00000000000 --- a/examples/gui/platform/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - #[cfg(not(windows))] - println!("cargo:rustc-link-lib=dylib=app"); - - #[cfg(windows)] - println!("cargo:rustc-link-lib=dylib=libapp"); - - println!("cargo:rustc-link-search=."); -} diff --git a/examples/gui/platform/host.c b/examples/gui/platform/host.c deleted file mode 100644 index b9214bcf335..00000000000 --- a/examples/gui/platform/host.c +++ /dev/null @@ -1,3 +0,0 @@ -extern int rust_main(); - -int main() { return rust_main(); } \ No newline at end of file diff --git a/examples/gui/platform/libapp.roc b/examples/gui/platform/libapp.roc new file mode 100644 index 00000000000..7c497fe5f0a --- /dev/null +++ b/examples/gui/platform/libapp.roc @@ -0,0 +1,3 @@ +app [render] { pf: platform "main.roc" } + +render = Text "stubbed app" \ No newline at end of file diff --git a/examples/gui/platform/src/main.rs b/examples/gui/platform/src/main.rs deleted file mode 100644 index 0765384f29f..00000000000 --- a/examples/gui/platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main() as _); -} diff --git a/examples/gui/platform/Cargo.toml b/examples/gui/roc_host/Cargo.toml similarity index 92% rename from examples/gui/platform/Cargo.toml rename to examples/gui/roc_host/Cargo.toml index 1110e5ddd06..785ea49b7f7 100644 --- a/examples/gui/platform/Cargo.toml +++ b/examples/gui/roc_host/Cargo.toml @@ -1,15 +1,16 @@ [package] -name = "host" +name = "roc_host" +version = "0.0.1" authors = ["The Roc Contributors"] -edition = "2021" license = "UPL-1.0" +edition = "2021" + links = "app" -version = "0.0.1" [lib] -name = "host" +name = "roc_host" path = "src/lib.rs" -crate-type = ["staticlib", "lib"] +crate-type = ["lib"] [[bin]] name = "host" @@ -45,8 +46,6 @@ default = [] features = ["derive"] version = "1.7.2" -[workspace] - # Optimizations based on https://deterministic.space/high-performance-rust.html [profile.release] codegen-units = 1 @@ -55,4 +54,4 @@ lto = "thin" # debug = true # enable when profiling [profile.bench] codegen-units = 1 -lto = "thin" +lto = "thin" \ No newline at end of file diff --git a/examples/gui/Inconsolata-Regular.ttf b/examples/gui/roc_host/Inconsolata-Regular.ttf similarity index 100% rename from examples/gui/Inconsolata-Regular.ttf rename to examples/gui/roc_host/Inconsolata-Regular.ttf diff --git a/examples/gui/roc_host/build.rs b/examples/gui/roc_host/build.rs new file mode 100644 index 00000000000..6e3962a9290 --- /dev/null +++ b/examples/gui/roc_host/build.rs @@ -0,0 +1,70 @@ +fn main() { + let workspace_dir = workspace_dir(); + let platform_dir = workspace_dir.join("platform"); + let app_stub_path = platform_dir.join("libapp.roc"); + let platform_main = platform_dir.join("main.roc"); + + println!("cargo:rustc-link-search={}", platform_dir.display()); + + #[cfg(not(windows))] + println!("cargo:rustc-link-lib=dylib=app"); + + #[cfg(windows)] + println!("cargo:rustc-link-lib=dylib=libapp"); + + // watch the platform/main.roc and rebuild app stub if it changes + println!("cargo:rerun-if-changed={}", platform_main.display()); + + // build the app stub dynamic library + std::process::Command::new("roc") + .args(&[ + "build", + "--lib", + format!("{}", app_stub_path.display()).as_str(), + ]) + .status() + .expect("unable to build the app stub dynamic library 'platform/libapp.roc'"); + + #[cfg(target_os = "macos")] + let app_stub_extension = "libapp.dylib"; + + #[cfg(target_os = "windows")] + let app_stub_extension = "libapp.obj"; + + #[cfg(target_os = "linux")] + let app_stub_extension = "libapp.so"; + + let expected_dylib_path = app_stub_path.with_file_name(app_stub_extension); + + // watch and rerun if there is a change in the dynamic library + println!("cargo:rerun-if-changed={}", expected_dylib_path.display()); + + // confirm the app stub dynamic library we built above is available + match std::fs::metadata(&expected_dylib_path) { + Ok(metadata) if metadata.is_file() => { + println!( + "cargo:warning=SUCCESSFULLY BUILT APP STUB DYNAMIC LIBRARY {:?}", + expected_dylib_path + ); + } + _ => { + println!( + "cargo:warning=APP STUB DYNAMIC LIBRARY WAS NOT BUILT CORRECTLY {:?}", + expected_dylib_path + ); + } + } +} + +/// helper to get the path to the workspace +fn workspace_dir() -> std::path::PathBuf { + let output = std::process::Command::new(env!("CARGO")) + .arg("locate-project") + .arg("--workspace") + .arg("--message-format=plain") + .output() + .unwrap() + .stdout; + let cargo_path = std::path::Path::new(std::str::from_utf8(&output).unwrap().trim()); + cargo_path.parent().unwrap().to_path_buf() +} diff --git a/examples/gui/platform/src/focus.rs b/examples/gui/roc_host/src/focus.rs similarity index 100% rename from examples/gui/platform/src/focus.rs rename to examples/gui/roc_host/src/focus.rs diff --git a/examples/gui/platform/src/graphics/colors.rs b/examples/gui/roc_host/src/graphics/colors.rs similarity index 100% rename from examples/gui/platform/src/graphics/colors.rs rename to examples/gui/roc_host/src/graphics/colors.rs diff --git a/examples/gui/platform/src/graphics/lowlevel/buffer.rs b/examples/gui/roc_host/src/graphics/lowlevel/buffer.rs similarity index 100% rename from examples/gui/platform/src/graphics/lowlevel/buffer.rs rename to examples/gui/roc_host/src/graphics/lowlevel/buffer.rs diff --git a/examples/gui/platform/src/graphics/lowlevel/mod.rs b/examples/gui/roc_host/src/graphics/lowlevel/mod.rs similarity index 100% rename from examples/gui/platform/src/graphics/lowlevel/mod.rs rename to examples/gui/roc_host/src/graphics/lowlevel/mod.rs index 0add45385d1..0215941baa8 100644 --- a/examples/gui/platform/src/graphics/lowlevel/mod.rs +++ b/examples/gui/roc_host/src/graphics/lowlevel/mod.rs @@ -1,5 +1,5 @@ pub mod buffer; pub mod ortho; pub mod pipelines; -pub mod vertex; pub mod quad; +pub mod vertex; diff --git a/examples/gui/platform/src/graphics/lowlevel/ortho.rs b/examples/gui/roc_host/src/graphics/lowlevel/ortho.rs similarity index 100% rename from examples/gui/platform/src/graphics/lowlevel/ortho.rs rename to examples/gui/roc_host/src/graphics/lowlevel/ortho.rs diff --git a/examples/gui/platform/src/graphics/lowlevel/pipelines.rs b/examples/gui/roc_host/src/graphics/lowlevel/pipelines.rs similarity index 100% rename from examples/gui/platform/src/graphics/lowlevel/pipelines.rs rename to examples/gui/roc_host/src/graphics/lowlevel/pipelines.rs diff --git a/examples/gui/platform/src/graphics/lowlevel/quad.rs b/examples/gui/roc_host/src/graphics/lowlevel/quad.rs similarity index 99% rename from examples/gui/platform/src/graphics/lowlevel/quad.rs rename to examples/gui/roc_host/src/graphics/lowlevel/quad.rs index e8c1f1b568b..f5f4f504b1f 100644 --- a/examples/gui/platform/src/graphics/lowlevel/quad.rs +++ b/examples/gui/roc_host/src/graphics/lowlevel/quad.rs @@ -1,5 +1,3 @@ - - /// A polygon with 4 corners #[repr(C)] #[derive(Copy, Clone)] diff --git a/examples/gui/platform/src/graphics/lowlevel/vertex.rs b/examples/gui/roc_host/src/graphics/lowlevel/vertex.rs similarity index 99% rename from examples/gui/platform/src/graphics/lowlevel/vertex.rs rename to examples/gui/roc_host/src/graphics/lowlevel/vertex.rs index aa45bb7fb73..f31b29b861d 100644 --- a/examples/gui/platform/src/graphics/lowlevel/vertex.rs +++ b/examples/gui/roc_host/src/graphics/lowlevel/vertex.rs @@ -11,7 +11,6 @@ // Thank you Héctor Ramón and Iced contributors! use bytemuck::{Pod, Zeroable}; - #[repr(C)] #[derive(Copy, Clone, Zeroable, Pod)] pub struct Vertex { diff --git a/examples/gui/platform/src/graphics/mod.rs b/examples/gui/roc_host/src/graphics/mod.rs similarity index 100% rename from examples/gui/platform/src/graphics/mod.rs rename to examples/gui/roc_host/src/graphics/mod.rs diff --git a/examples/gui/platform/src/graphics/primitives/mod.rs b/examples/gui/roc_host/src/graphics/primitives/mod.rs similarity index 100% rename from examples/gui/platform/src/graphics/primitives/mod.rs rename to examples/gui/roc_host/src/graphics/primitives/mod.rs diff --git a/examples/gui/platform/src/graphics/primitives/rect.rs b/examples/gui/roc_host/src/graphics/primitives/rect.rs similarity index 100% rename from examples/gui/platform/src/graphics/primitives/rect.rs rename to examples/gui/roc_host/src/graphics/primitives/rect.rs diff --git a/examples/gui/platform/src/graphics/primitives/text.rs b/examples/gui/roc_host/src/graphics/primitives/text.rs similarity index 96% rename from examples/gui/platform/src/graphics/primitives/text.rs rename to examples/gui/roc_host/src/graphics/primitives/text.rs index c746063b77b..683a5a6c3ec 100644 --- a/examples/gui/platform/src/graphics/primitives/text.rs +++ b/examples/gui/roc_host/src/graphics/primitives/text.rs @@ -129,9 +129,7 @@ pub fn build_glyph_brush( gpu_device: &wgpu::Device, render_format: wgpu::TextureFormat, ) -> Result, InvalidFont> { - let inconsolata = FontArc::try_from_slice(include_bytes!( - "../../../../Inconsolata-Regular.ttf" - ))?; + let inconsolata = FontArc::try_from_slice(include_bytes!("../../../Inconsolata-Regular.ttf"))?; Ok(GlyphBrushBuilder::using_font(inconsolata).build(gpu_device, render_format)) } diff --git a/examples/gui/platform/src/graphics/shaders/quad.wgsl b/examples/gui/roc_host/src/graphics/shaders/quad.wgsl similarity index 100% rename from examples/gui/platform/src/graphics/shaders/quad.wgsl rename to examples/gui/roc_host/src/graphics/shaders/quad.wgsl diff --git a/examples/gui/platform/src/graphics/style.rs b/examples/gui/roc_host/src/graphics/style.rs similarity index 100% rename from examples/gui/platform/src/graphics/style.rs rename to examples/gui/roc_host/src/graphics/style.rs diff --git a/examples/gui/platform/src/gui.rs b/examples/gui/roc_host/src/gui.rs similarity index 99% rename from examples/gui/platform/src/gui.rs rename to examples/gui/roc_host/src/gui.rs index e054a1895be..fe0a1b4182f 100644 --- a/examples/gui/platform/src/gui.rs +++ b/examples/gui/roc_host/src/gui.rs @@ -55,11 +55,13 @@ fn run_event_loop(title: &str, root: RocElem) -> Result<(), Box> { force_fallback_adapter: false, }) .await - .expect(r#"Request adapter + .expect( + r#"Request adapter If you're running this from inside nix, run with: `nixVulkanIntel `. See extra docs here: github.com/guibou/nixGL - "#); + "#, + ); adapter .request_device( @@ -294,7 +296,9 @@ fn run_event_loop(title: &str, root: RocElem) -> Result<(), Box> { } }); - Ok(()) + // Done, let's just exit... and don't bother cleaning anything up + // for this simple example + std::process::exit(0); } fn draw_rects( diff --git a/examples/gui/platform/src/lib.rs b/examples/gui/roc_host/src/lib.rs similarity index 82% rename from examples/gui/platform/src/lib.rs rename to examples/gui/roc_host/src/lib.rs index 17fa0b4d4ad..bf30db3946a 100644 --- a/examples/gui/platform/src/lib.rs +++ b/examples/gui/roc_host/src/lib.rs @@ -11,11 +11,8 @@ extern "C" { } #[no_mangle] -pub extern "C" fn rust_main() -> i32 { +pub extern "C" fn rust_main() { let root_elem = unsafe { roc_render() }; gui::render("test title".into(), root_elem); - - // Exit code - 0 } diff --git a/examples/gui/roc_host/src/main.rs b/examples/gui/roc_host/src/main.rs new file mode 100644 index 00000000000..245351db645 --- /dev/null +++ b/examples/gui/roc_host/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + roc_host::rust_main(); +} diff --git a/examples/gui/platform/src/rects_and_texts.rs b/examples/gui/roc_host/src/rects_and_texts.rs similarity index 100% rename from examples/gui/platform/src/rects_and_texts.rs rename to examples/gui/roc_host/src/rects_and_texts.rs diff --git a/examples/gui/platform/src/roc.rs b/examples/gui/roc_host/src/roc.rs similarity index 100% rename from examples/gui/platform/src/roc.rs rename to examples/gui/roc_host/src/roc.rs diff --git a/examples/gui/roc_host_lib/Cargo.toml b/examples/gui/roc_host_lib/Cargo.toml new file mode 100644 index 00000000000..5cf9b7b97b9 --- /dev/null +++ b/examples/gui/roc_host_lib/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "host" +version = "0.0.1" +authors = ["The Roc Contributors"] +license = "UPL-1.0" +edition = "2021" + +[profile.release] +lto = true +strip = "debuginfo" +codegen-units = 1 + +[lib] +name = "host" +path = "src/lib.rs" +crate-type = ["staticlib"] + +[dependencies] +roc_host = { path = "../roc_host" } diff --git a/examples/gui/roc_host_lib/src/lib.rs b/examples/gui/roc_host_lib/src/lib.rs new file mode 100644 index 00000000000..debb8563d06 --- /dev/null +++ b/examples/gui/roc_host_lib/src/lib.rs @@ -0,0 +1,4 @@ +#[no_mangle] +pub extern "C" fn main() { + roc_host::rust_main(); +} diff --git a/examples/platform-switching/README.md b/examples/platform-switching/README.md index 1db17157f8f..8050baa278a 100644 --- a/examples/platform-switching/README.md +++ b/examples/platform-switching/README.md @@ -1,20 +1,25 @@ # Platform switching -To run, `cd` into this directory and run this in your terminal: +To run, `cd` into one of the examples in this directory and run this in your terminal: ```bash -roc run +roc build.roc ``` -This will run `main.roc` because, unless you explicitly give it a filename, `roc run` -defaults to running a file named `main.roc`. Other `roc` commands (like `roc build`, `roc test`, and so on) also default to `main.roc` unless you explicitly give them a filename. +This will build the platform into a standalone library. + +Then you can run the roc app using e.g. + +```bash +roc rocLovesZig.roc +``` ## About this example -This uses a very simple platform which does nothing more than printing the string you give it. +This uses a very simple platform which does nothing more than printing the string you give it. -The line `main = "Which platform am I running on now?\n"` sets this string to be `"Which platform am I running on now?"` with a newline at the end, and the lines `packages { pf: "c-platform/main.roc" }` and `provides [main] to pf` specify that the `c-platform/` directory contains this app's platform. +The line `main = "Which platform am I running on now?\n"` sets this string to be `"Which platform am I running on now?"` with a newline at the end, and the lines `packages { pf: "platform/main.roc" }` and `provides [main] to pf` specify that the `platform/` directory contains this app's platform, and importantly the host's prebuilt-binaries. -This platform is called `c-platform` because its lower-level code is written in C. There's also a `rust-platform`, `zig-platform`, and so on; if you like, you can try switching `pf: "c-platform/main.roc"` to `pf: "zig-platform/main.roc"` or `pf: "rust-platform/main.roc"` to try one of those platforms instead. They all do similar things, so the application won't look any different. +This host is called `host` because its lower-level code is written in a systems programming language like C, Zig, Rust, Swift, Go. You can look through all the examples and you will see the `platform/main.roc` is identical. This shows how you can the exact same roc application and platform even if you swap out the implementation of the host. -If you want to start building your own platforms, these are some very simple example platforms to use as starting points. +If you want to start building your own platforms, these are some very simple example platforms to use as starting points. If you are looking for more advanced examples, consider asking in the roc zulip or checkout the [official examples repository](https://www.roc-lang.org/examples). diff --git a/examples/platform-switching/c/.gitignore b/examples/platform-switching/c/.gitignore new file mode 100644 index 00000000000..77200f384ec --- /dev/null +++ b/examples/platform-switching/c/.gitignore @@ -0,0 +1,3 @@ +build +*.a +rocLovesC \ No newline at end of file diff --git a/examples/platform-switching/c/build.roc b/examples/platform-switching/c/build.roc new file mode 100644 index 00000000000..bdd7d67f72d --- /dev/null +++ b/examples/platform-switching/c/build.roc @@ -0,0 +1,49 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # build the host + Cmd.exec "zig" ["build-lib", "-lc", "host/host.c"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/platform-switching/c-platform/host.c b/examples/platform-switching/c/host/host.c similarity index 100% rename from examples/platform-switching/c-platform/host.c rename to examples/platform-switching/c/host/host.c diff --git a/examples/platform-switching/c-platform/main.roc b/examples/platform-switching/c/platform/main.roc similarity index 87% rename from examples/platform-switching/c-platform/main.roc rename to examples/platform-switching/c/platform/main.roc index 35c286a30ef..e141577dcb2 100644 --- a/examples/platform-switching/c-platform/main.roc +++ b/examples/platform-switching/c/platform/main.roc @@ -1,4 +1,4 @@ -platform "echo-in-c" +platform "" requires {} { main : Str } exposes [] packages {} diff --git a/examples/platform-switching/c/rocLovesC.roc b/examples/platform-switching/c/rocLovesC.roc new file mode 100644 index 00000000000..1637f00e5f4 --- /dev/null +++ b/examples/platform-switching/c/rocLovesC.roc @@ -0,0 +1,3 @@ +app [main] { pf: platform "platform/main.roc" } + +main = "Roc <3 C!\n" diff --git a/examples/platform-switching/rocLovesC.roc b/examples/platform-switching/rocLovesC.roc deleted file mode 100644 index 8c7bb7a81fc..00000000000 --- a/examples/platform-switching/rocLovesC.roc +++ /dev/null @@ -1,3 +0,0 @@ -app [main] { pf: platform "c-platform/main.roc" } - -main = "Roc <3 C!\n" diff --git a/examples/platform-switching/rocLovesRust.roc b/examples/platform-switching/rocLovesRust.roc deleted file mode 100644 index a3387684019..00000000000 --- a/examples/platform-switching/rocLovesRust.roc +++ /dev/null @@ -1,3 +0,0 @@ -app [main] { pf: platform "rust-platform/main.roc" } - -main = "Roc <3 Rust!\n" diff --git a/examples/platform-switching/rocLovesSwift.roc b/examples/platform-switching/rocLovesSwift.roc deleted file mode 100644 index fd09c43f01d..00000000000 --- a/examples/platform-switching/rocLovesSwift.roc +++ /dev/null @@ -1,3 +0,0 @@ -app [main] { pf: platform "swift-platform/main.roc" } - -main = "Roc <3 Swift!\n" diff --git a/examples/platform-switching/rocLovesZig.roc b/examples/platform-switching/rocLovesZig.roc deleted file mode 100644 index aa0526fe972..00000000000 --- a/examples/platform-switching/rocLovesZig.roc +++ /dev/null @@ -1,3 +0,0 @@ -app [main] { pf: platform "zig-platform/main.roc" } - -main = "Roc <3 Zig!\n" diff --git a/examples/platform-switching/rust-platform/build.rs b/examples/platform-switching/rust-platform/build.rs deleted file mode 100644 index 47763b34c39..00000000000 --- a/examples/platform-switching/rust-platform/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - #[cfg(not(windows))] - println!("cargo:rustc-link-lib=dylib=app"); - - #[cfg(windows)] - println!("cargo:rustc-link-lib=dylib=libapp"); - - println!("cargo:rustc-link-search=."); -} diff --git a/examples/platform-switching/rust-platform/host.c b/examples/platform-switching/rust-platform/host.c deleted file mode 100644 index b9214bcf335..00000000000 --- a/examples/platform-switching/rust-platform/host.c +++ /dev/null @@ -1,3 +0,0 @@ -extern int rust_main(); - -int main() { return rust_main(); } \ No newline at end of file diff --git a/examples/platform-switching/rust-platform/rust-toolchain.toml b/examples/platform-switching/rust-platform/rust-toolchain.toml deleted file mode 100644 index d564cb7f7da..00000000000 --- a/examples/platform-switching/rust-platform/rust-toolchain.toml +++ /dev/null @@ -1,9 +0,0 @@ -[toolchain] -channel = "1.76.0" - -profile = "default" - -components = [ - # for usages of rust-analyzer or similar tools inside `nix develop` - "rust-src" -] \ No newline at end of file diff --git a/examples/platform-switching/rust-platform/src/glue.rs b/examples/platform-switching/rust-platform/src/glue.rs deleted file mode 100644 index e3e2e524b7e..00000000000 --- a/examples/platform-switching/rust-platform/src/glue.rs +++ /dev/null @@ -1,744 +0,0 @@ -// ⚠️ GENERATED CODE ⚠️ - this entire file was generated by the `roc glue` CLI command - -#![allow(unused_unsafe)] -#![allow(unused_variables)] -#![allow(dead_code)] -#![allow(unused_mut)] -#![allow(non_snake_case)] -#![allow(non_camel_case_types)] -#![allow(non_upper_case_globals)] -#![allow(clippy::undocumented_unsafe_blocks)] -#![allow(clippy::redundant_static_lifetimes)] -#![allow(clippy::unused_unit)] -#![allow(clippy::missing_safety_doc)] -#![allow(clippy::let_and_return)] -#![allow(clippy::missing_safety_doc)] -#![allow(clippy::redundant_static_lifetimes)] -#![allow(clippy::needless_borrow)] -#![allow(clippy::clone_on_copy)] - -type Op_StderrWrite = roc_std::RocStr; -type Op_StdoutWrite = roc_std::RocStr; -type TODO_roc_function_69 = roc_std::RocStr; -type TODO_roc_function_70 = roc_std::RocStr; - -#[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" -))] -#[derive(Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] -#[repr(u8)] -pub enum discriminant_Op { - Done = 0, - StderrWrite = 1, - StdoutWrite = 2, -} - -impl core::fmt::Debug for discriminant_Op { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Self::Done => f.write_str("discriminant_Op::Done"), - Self::StderrWrite => f.write_str("discriminant_Op::StderrWrite"), - Self::StdoutWrite => f.write_str("discriminant_Op::StdoutWrite"), - } - } -} - -#[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" -))] -#[repr(transparent)] -pub struct Op { - pointer: *mut union_Op, -} - -#[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" -))] -#[repr(C)] -union union_Op { - StderrWrite: core::mem::ManuallyDrop, - StdoutWrite: core::mem::ManuallyDrop, - _sizer: [u8; 8], -} - -#[cfg(any( - target_arch = "arm", - target_arch = "arm", - target_arch = "aarch64", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86", - target_arch = "x86_64", - target_arch = "x86_64" -))] -//TODO HAS CLOSURE 2 -#[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" -))] -#[repr(C)] -pub struct RocFunction_66 { - pub closure_data: roc_std::RocList, -} - -impl RocFunction_66 { - pub fn force_thunk(mut self, arg_0: ()) -> Op { - extern "C" { - fn roc__mainForHost_0_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); - } - - let mut output = std::mem::MaybeUninit::uninit(); - let ptr = self.closure_data.as_mut_ptr(); - unsafe { roc__mainForHost_0_caller(&arg_0, ptr, output.as_mut_ptr()) }; - unsafe { output.assume_init() } - } -} - -#[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" -))] -#[repr(C)] -pub struct RocFunction_67 { - pub closure_data: roc_std::RocList, -} - -impl RocFunction_67 { - pub fn force_thunk(mut self, arg_0: ()) -> Op { - extern "C" { - fn roc__mainForHost_1_caller(arg_0: &(), closure_data: *mut u8, output: *mut Op); - } - - let mut output = std::mem::MaybeUninit::uninit(); - let ptr = self.closure_data.as_mut_ptr(); - unsafe { roc__mainForHost_1_caller(&arg_0, ptr, output.as_mut_ptr()) }; - unsafe { output.assume_init() } - } -} - -impl Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - #[inline(always)] - fn storage(&self) -> Option<&core::cell::Cell> { - let mask = match std::mem::size_of::() { - 4 => 0b11, - 8 => 0b111, - _ => unreachable!(), - }; - - // NOTE: pointer provenance is probably lost here - let unmasked_address = (self.pointer as usize) & !mask; - let untagged = unmasked_address as *const core::cell::Cell; - - if untagged.is_null() { - None - } else { - unsafe { Some(&*untagged.sub(1)) } - } - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Returns which variant this tag union holds. Note that this never includes a payload! - pub fn discriminant(&self) -> discriminant_Op { - // The discriminant is stored in the unused bytes at the end of the recursive pointer - unsafe { core::mem::transmute::((self.pointer as u8) & 0b11) } - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Internal helper - fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { - // The discriminant is stored in the unused bytes at the end of the union pointer - let untagged = (pointer as usize) & (!0b11 as usize); - let tagged = untagged | (discriminant as usize); - - tagged as *mut union_Op - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Internal helper - fn union_pointer(&self) -> *mut union_Op { - // The discriminant is stored in the unused bytes at the end of the union pointer - ((self.pointer as usize) & (!0b11 as usize)) as *mut union_Op - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// A tag named Done, which has no payload. - pub const Done: Self = Self { - pointer: core::ptr::null_mut(), - }; - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 0. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn get_StderrWrite_0(&self) -> roc_std::RocStr { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - - extern "C" { - #[link_name = "roc__getter__2_generic"] - fn getter(_: *mut roc_std::RocStr, _: *const Op); - } - - let mut ret = core::mem::MaybeUninit::uninit(); - getter(ret.as_mut_ptr(), self); - ret.assume_init() - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload at index 1. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn get_StderrWrite_1(&self) -> RocFunction_67 { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - - extern "C" { - #[link_name = "roc__getter__3_size"] - fn size() -> usize; - - #[link_name = "roc__getter__3_generic"] - fn getter(_: *mut u8, _: *const Op); - } - - // allocate memory to store this variably-sized value - // allocates with roc_alloc, but that likely still uses the heap - let it = std::iter::repeat(0xAAu8).take(size()); - let mut bytes = roc_std::RocList::from_iter(it); - - getter(bytes.as_mut_ptr(), self); - - RocFunction_67 { - closure_data: bytes, - } - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Construct a tag named `StderrWrite`, with the appropriate payload - pub fn StderrWrite(arg: Op_StderrWrite) -> Self { - let size = core::mem::size_of::(); - let align = core::mem::align_of::() as u32; - - unsafe { - let ptr = roc_std::roc_alloc_refcounted::(); - - *ptr = union_Op { - StderrWrite: core::mem::ManuallyDrop::new(arg), - }; - - Self { - pointer: Self::tag_discriminant(ptr, discriminant_Op::StderrWrite), - } - } - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - let payload = { - let ptr = (self.pointer as usize & !0b11) as *mut union_Op; - let mut uninitialized = core::mem::MaybeUninit::uninit(); - let swapped = unsafe { - core::mem::replace( - &mut (*ptr).StderrWrite, - core::mem::ManuallyDrop::new(uninitialized.assume_init()), - ) - }; - - core::mem::forget(self); - - core::mem::ManuallyDrop::into_inner(swapped) - }; - - payload - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - let payload = { - let ptr = (self.pointer as usize & !0b11) as *mut union_Op; - - unsafe { &(*ptr).StderrWrite } - }; - - &payload - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 0. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn get_StdoutWrite_0(&self) -> roc_std::RocStr { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - - extern "C" { - #[link_name = "roc__getter__2_generic"] - fn getter(_: *mut roc_std::RocStr, _: *const Op); - } - - let mut ret = core::mem::MaybeUninit::uninit(); - getter(ret.as_mut_ptr(), self); - ret.assume_init() - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload at index 1. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn get_StdoutWrite_1(&self) -> RocFunction_66 { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - - extern "C" { - #[link_name = "roc__getter__3_size"] - fn size() -> usize; - - #[link_name = "roc__getter__3_generic"] - fn getter(_: *mut u8, _: *const Op); - } - - // allocate memory to store this variably-sized value - // allocates with roc_alloc, but that likely still uses the heap - let it = std::iter::repeat(0xAAu8).take(size()); - let mut bytes = roc_std::RocList::from_iter(it); - - getter(bytes.as_mut_ptr(), self); - - RocFunction_66 { - closure_data: bytes, - } - } - - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - /// Construct a tag named `StdoutWrite`, with the appropriate payload - pub fn StdoutWrite(arg: Op_StdoutWrite) -> Self { - let size = core::mem::size_of::(); - let align = core::mem::align_of::() as u32; - - unsafe { - let ptr = roc_std::roc_alloc_refcounted::(); - - *ptr = union_Op { - StdoutWrite: core::mem::ManuallyDrop::new(arg), - }; - - Self { - pointer: Self::tag_discriminant(ptr, discriminant_Op::StdoutWrite), - } - } - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - let payload = { - let ptr = (self.pointer as usize & !0b11) as *mut union_Op; - let mut uninitialized = core::mem::MaybeUninit::uninit(); - let swapped = unsafe { - core::mem::replace( - &mut (*ptr).StdoutWrite, - core::mem::ManuallyDrop::new(uninitialized.assume_init()), - ) - }; - - core::mem::forget(self); - - core::mem::ManuallyDrop::into_inner(swapped) - }; - - payload - } - - #[cfg(any(target_arch = "arm", target_arch = "wasm32", target_arch = "x86"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - let payload = { - let ptr = (self.pointer as usize & !0b11) as *mut union_Op; - - unsafe { &(*ptr).StdoutWrite } - }; - - &payload - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Returns which variant this tag union holds. Note that this never includes a payload! - pub fn discriminant(&self) -> discriminant_Op { - // The discriminant is stored in the unused bytes at the end of the recursive pointer - unsafe { core::mem::transmute::((self.pointer as u8) & 0b111) } - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Internal helper - fn tag_discriminant(pointer: *mut union_Op, discriminant: discriminant_Op) -> *mut union_Op { - // The discriminant is stored in the unused bytes at the end of the union pointer - let untagged = (pointer as usize) & (!0b111 as usize); - let tagged = untagged | (discriminant as usize); - - tagged as *mut union_Op - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Internal helper - fn union_pointer(&self) -> *mut union_Op { - // The discriminant is stored in the unused bytes at the end of the union pointer - ((self.pointer as usize) & (!0b111 as usize)) as *mut union_Op - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and convert it to `StderrWrite`'s payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn into_StderrWrite(mut self) -> Op_StderrWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - let payload = { - let ptr = (self.pointer as usize & !0b111) as *mut union_Op; - let mut uninitialized = core::mem::MaybeUninit::uninit(); - let swapped = unsafe { - core::mem::replace( - &mut (*ptr).StderrWrite, - core::mem::ManuallyDrop::new(uninitialized.assume_init()), - ) - }; - - core::mem::forget(self); - - core::mem::ManuallyDrop::into_inner(swapped) - }; - - payload - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StderrWrite` and return its payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StderrWrite`. - pub unsafe fn as_StderrWrite(&self) -> &Op_StderrWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StderrWrite); - let payload = { - let ptr = (self.pointer as usize & !0b111) as *mut union_Op; - - unsafe { &(*ptr).StderrWrite } - }; - - &payload - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and convert it to `StdoutWrite`'s payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn into_StdoutWrite(mut self) -> Op_StdoutWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - let payload = { - let ptr = (self.pointer as usize & !0b111) as *mut union_Op; - let mut uninitialized = core::mem::MaybeUninit::uninit(); - let swapped = unsafe { - core::mem::replace( - &mut (*ptr).StdoutWrite, - core::mem::ManuallyDrop::new(uninitialized.assume_init()), - ) - }; - - core::mem::forget(self); - - core::mem::ManuallyDrop::into_inner(swapped) - }; - - payload - } - - #[cfg(any(target_arch = "aarch64", target_arch = "x86_64"))] - /// Unsafely assume this `Op` has a `.discriminant()` of `StdoutWrite` and return its payload. - /// (Always examine `.discriminant()` first to make sure this is the correct variant!) - /// Panics in debug builds if the `.discriminant()` doesn't return `StdoutWrite`. - pub unsafe fn as_StdoutWrite(&self) -> &Op_StdoutWrite { - debug_assert_eq!(self.discriminant(), discriminant_Op::StdoutWrite); - let payload = { - let ptr = (self.pointer as usize & !0b111) as *mut union_Op; - - unsafe { &(*ptr).StdoutWrite } - }; - - &payload - } -} - -impl Drop for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn drop(&mut self) { - // We only need to do any work if there's actually a heap-allocated payload. - if let Some(storage) = self.storage() { - let mut new_storage = storage.get(); - - // Decrement the refcount - let needs_dealloc = !new_storage.is_readonly() && new_storage.decrease(); - - if needs_dealloc { - // Drop the payload first. - match self.discriminant() { - discriminant_Op::Done => {} - discriminant_Op::StderrWrite => unsafe { - core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StderrWrite) - }, - discriminant_Op::StdoutWrite => unsafe { - core::mem::ManuallyDrop::drop(&mut (&mut *self.union_pointer()).StdoutWrite) - }, - } - - // Dealloc the pointer - let alignment = - core::mem::align_of::().max(core::mem::align_of::()); - - unsafe { - crate::roc_dealloc(storage.as_ptr().cast(), alignment as u32); - } - } else { - // Write the storage back. - storage.set(new_storage); - } - } - } -} - -impl Eq for Op {} - -impl PartialEq for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn eq(&self, other: &Self) -> bool { - if self.discriminant() != other.discriminant() { - return false; - } - - unsafe { - match self.discriminant() { - discriminant_Op::Done => true, - discriminant_Op::StderrWrite => { - (&*self.union_pointer()).StderrWrite == (&*other.union_pointer()).StderrWrite - } - discriminant_Op::StdoutWrite => { - (&*self.union_pointer()).StdoutWrite == (&*other.union_pointer()).StdoutWrite - } - } - } - } -} - -impl PartialOrd for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn partial_cmp(&self, other: &Self) -> Option { - match self.discriminant().partial_cmp(&other.discriminant()) { - Some(core::cmp::Ordering::Equal) => {} - not_eq => return not_eq, - } - - unsafe { - match self.discriminant() { - discriminant_Op::Done => Some(core::cmp::Ordering::Equal), - discriminant_Op::StderrWrite => (&*self.union_pointer()) - .StderrWrite - .partial_cmp(&(&*other.union_pointer()).StderrWrite), - discriminant_Op::StdoutWrite => (&*self.union_pointer()) - .StdoutWrite - .partial_cmp(&(&*other.union_pointer()).StdoutWrite), - } - } - } -} - -impl Ord for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn cmp(&self, other: &Self) -> core::cmp::Ordering { - match self.discriminant().cmp(&other.discriminant()) { - core::cmp::Ordering::Equal => {} - not_eq => return not_eq, - } - - unsafe { - match self.discriminant() { - discriminant_Op::Done => core::cmp::Ordering::Equal, - discriminant_Op::StderrWrite => (&*self.union_pointer()) - .StderrWrite - .cmp(&(&*other.union_pointer()).StderrWrite), - discriminant_Op::StdoutWrite => (&*self.union_pointer()) - .StdoutWrite - .cmp(&(&*other.union_pointer()).StdoutWrite), - } - } - } -} - -impl Clone for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn clone(&self) -> Self { - if let Some(storage) = self.storage() { - let mut new_storage = storage.get(); - if !new_storage.is_readonly() { - new_storage.increment_reference_count(); - storage.set(new_storage); - } - } - - Self { - pointer: self.pointer, - } - } -} - -impl core::hash::Hash for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn hash(&self, state: &mut H) { - match self.discriminant() { - discriminant_Op::Done => discriminant_Op::Done.hash(state), - discriminant_Op::StderrWrite => unsafe { - discriminant_Op::StderrWrite.hash(state); - (&*self.union_pointer()).StderrWrite.hash(state); - }, - discriminant_Op::StdoutWrite => unsafe { - discriminant_Op::StdoutWrite.hash(state); - (&*self.union_pointer()).StdoutWrite.hash(state); - }, - } - } -} - -impl core::fmt::Debug for Op { - #[cfg(any( - target_arch = "arm", - target_arch = "aarch64", - target_arch = "wasm32", - target_arch = "x86", - target_arch = "x86_64" - ))] - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_str("Op::")?; - - unsafe { - match self.discriminant() { - discriminant_Op::Done => f.write_str("Done"), - discriminant_Op::StderrWrite => f - .debug_tuple("StderrWrite") - // TODO HAS CLOSURE - .finish(), - discriminant_Op::StdoutWrite => f - .debug_tuple("StdoutWrite") - // TODO HAS CLOSURE - .finish(), - } - } - } -} diff --git a/examples/platform-switching/rust-platform/src/main.rs b/examples/platform-switching/rust-platform/src/main.rs deleted file mode 100644 index 0765384f29f..00000000000 --- a/examples/platform-switching/rust-platform/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - std::process::exit(host::rust_main() as _); -} diff --git a/examples/platform-switching/rust/.gitignore b/examples/platform-switching/rust/.gitignore new file mode 100644 index 00000000000..93e77ed8a6b --- /dev/null +++ b/examples/platform-switching/rust/.gitignore @@ -0,0 +1,3 @@ +target/ +platform/*.a +build \ No newline at end of file diff --git a/examples/platform-switching/rust-platform/Cargo.toml b/examples/platform-switching/rust/Cargo.toml similarity index 66% rename from examples/platform-switching/rust-platform/Cargo.toml rename to examples/platform-switching/rust/Cargo.toml index c5dca2b05ab..92d1e771342 100644 --- a/examples/platform-switching/rust-platform/Cargo.toml +++ b/examples/platform-switching/rust/Cargo.toml @@ -3,17 +3,12 @@ name = "host" authors = ["The Roc Contributors"] edition = "2021" license = "UPL-1.0" -links = "app" version = "0.0.1" [lib] name = "host" -path = "src/lib.rs" -crate-type = ["staticlib", "lib"] - -[[bin]] -name = "host" -path = "src/main.rs" +path = "host/lib.rs" +crate-type = ["staticlib"] [dependencies] libc = "0.2" diff --git a/examples/platform-switching/rust/build.roc b/examples/platform-switching/rust/build.roc new file mode 100644 index 00000000000..cf77ce0fc90 --- /dev/null +++ b/examples/platform-switching/rust/build.roc @@ -0,0 +1,50 @@ +app "build-quicksort" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + + # build the host + Cmd.exec "cargo" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "target/debug/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/platform-switching/rust-platform/src/lib.rs b/examples/platform-switching/rust/host/lib.rs similarity index 95% rename from examples/platform-switching/rust-platform/src/lib.rs rename to examples/platform-switching/rust/host/lib.rs index f3c83ed5574..8ed1afdbcd1 100644 --- a/examples/platform-switching/rust-platform/src/lib.rs +++ b/examples/platform-switching/rust/host/lib.rs @@ -2,9 +2,7 @@ use core::ffi::c_void; use roc_std::RocStr; -use std::ffi::CStr; use std::io::Write; -use std::os::raw::c_char; extern "C" { #[link_name = "roc__mainForHost_1_exposed_generic"] @@ -85,14 +83,11 @@ pub unsafe extern "C" fn roc_shm_open( } #[no_mangle] -pub extern "C" fn rust_main() -> i32 { +pub extern "C" fn main() { let mut roc_str = RocStr::default(); unsafe { roc_main(&mut roc_str) }; if let Err(e) = std::io::stdout().write_all(roc_str.as_bytes()) { panic!("Writing to stdout failed! {:?}", e); } - - // Exit code - 0 } diff --git a/examples/platform-switching/zig-platform/main.roc b/examples/platform-switching/rust/platform/main.roc similarity index 86% rename from examples/platform-switching/zig-platform/main.roc rename to examples/platform-switching/rust/platform/main.roc index a52fe9a4801..e141577dcb2 100644 --- a/examples/platform-switching/zig-platform/main.roc +++ b/examples/platform-switching/rust/platform/main.roc @@ -1,4 +1,4 @@ -platform "echo-in-zig" +platform "" requires {} { main : Str } exposes [] packages {} diff --git a/examples/platform-switching/rust/rocLovesRust.roc b/examples/platform-switching/rust/rocLovesRust.roc new file mode 100644 index 00000000000..0739e41139c --- /dev/null +++ b/examples/platform-switching/rust/rocLovesRust.roc @@ -0,0 +1,3 @@ +app [main] { pf: platform "platform/main.roc" } + +main = "Roc <3 Rust!\n" diff --git a/examples/platform-switching/swift/.gitignore b/examples/platform-switching/swift/.gitignore new file mode 100644 index 00000000000..0f61cf0048e --- /dev/null +++ b/examples/platform-switching/swift/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc + +platform/*.a \ No newline at end of file diff --git a/examples/platform-switching/swift/Package.swift b/examples/platform-switching/swift/Package.swift new file mode 100644 index 00000000000..61690f314e4 --- /dev/null +++ b/examples/platform-switching/swift/Package.swift @@ -0,0 +1,21 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "swift-host", + platforms: [ + .macOS(.v13), + ], + products: [ + .library( + name: "host", + type: .static, + targets: ["Host","Roc"] + ), + ], + targets: [ + .target(name: "Host"), + .target(name: "Roc", dependencies: []) + ] +) \ No newline at end of file diff --git a/examples/platform-switching/swift-platform/host.swift b/examples/platform-switching/swift/Sources/Host/host.swift similarity index 99% rename from examples/platform-switching/swift-platform/host.swift rename to examples/platform-switching/swift/Sources/Host/host.swift index 144ed8a54a4..58f4bf02171 100644 --- a/examples/platform-switching/swift-platform/host.swift +++ b/examples/platform-switching/swift/Sources/Host/host.swift @@ -1,4 +1,5 @@ import Foundation +import Roc @_cdecl("roc_alloc") func rocAlloc(size: Int, _alignment: UInt) -> UInt { diff --git a/examples/platform-switching/swift-platform/host.h b/examples/platform-switching/swift/Sources/Roc/include/roc_std.h similarity index 100% rename from examples/platform-switching/swift-platform/host.h rename to examples/platform-switching/swift/Sources/Roc/include/roc_std.h diff --git a/examples/platform-switching/swift/Sources/Roc/roc_std.c b/examples/platform-switching/swift/Sources/Roc/roc_std.c new file mode 100644 index 00000000000..e69de29bb2d diff --git a/examples/platform-switching/swift/build.roc b/examples/platform-switching/swift/build.roc new file mode 100644 index 00000000000..59eeae37204 --- /dev/null +++ b/examples/platform-switching/swift/build.roc @@ -0,0 +1,49 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # build the host + Cmd.exec "swift" ["build","-c","release"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "./.build/release/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/platform-switching/rust-platform/main.roc b/examples/platform-switching/swift/platform/main.roc similarity index 85% rename from examples/platform-switching/rust-platform/main.roc rename to examples/platform-switching/swift/platform/main.roc index 13dd322d6b9..e141577dcb2 100644 --- a/examples/platform-switching/rust-platform/main.roc +++ b/examples/platform-switching/swift/platform/main.roc @@ -1,4 +1,4 @@ -platform "echo-in-rust" +platform "" requires {} { main : Str } exposes [] packages {} diff --git a/examples/platform-switching/swift/rocLovesSwift.roc b/examples/platform-switching/swift/rocLovesSwift.roc new file mode 100644 index 00000000000..ba2e3dd5ff1 --- /dev/null +++ b/examples/platform-switching/swift/rocLovesSwift.roc @@ -0,0 +1,3 @@ +app [main] { pf: platform "platform/main.roc" } + +main = "Roc <3 Swift!\n" diff --git a/examples/platform-switching/web-assembly-platform/main.roc b/examples/platform-switching/web-assembly-platform/main.roc deleted file mode 100644 index 08830392cda..00000000000 --- a/examples/platform-switching/web-assembly-platform/main.roc +++ /dev/null @@ -1,9 +0,0 @@ -platform "echo-in-web-assembly" - requires {} { main : Str } - exposes [] - packages {} - imports [] - provides [mainForHost] - -mainForHost : Str -mainForHost = main diff --git a/examples/platform-switching/web-assembly/.gitignore b/examples/platform-switching/web-assembly/.gitignore new file mode 100644 index 00000000000..896d62cc8a1 --- /dev/null +++ b/examples/platform-switching/web-assembly/.gitignore @@ -0,0 +1,5 @@ +zig-cache/ +platform/*.wasm +zig-out/ +build +rocLovesWebAssembly \ No newline at end of file diff --git a/examples/platform-switching/web-assembly-platform/README.md b/examples/platform-switching/web-assembly/README.md similarity index 100% rename from examples/platform-switching/web-assembly-platform/README.md rename to examples/platform-switching/web-assembly/README.md diff --git a/examples/platform-switching/web-assembly/build.roc b/examples/platform-switching/web-assembly/build.roc new file mode 100644 index 00000000000..1db8e9d73c6 --- /dev/null +++ b/examples/platform-switching/web-assembly/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/platform-switching/web-assembly/build.zig b/examples/platform-switching/web-assembly/build.zig new file mode 100644 index 00000000000..fb6f3746285 --- /dev/null +++ b/examples/platform-switching/web-assembly/build.zig @@ -0,0 +1,21 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + // hardcode build for -Dtarget=wasm32-wasi-musl target + .target = std.zig.CrossTarget{ + .cpu_arch = .wasm32, + .os_tag = .wasi, + .abi = .musl, + }, + .optimize = optimize, + }); + + lib.dead_strip_dylibs = false; + + b.installArtifact(lib); +} diff --git a/examples/platform-switching/web-assembly/host/glue/list.zig b/examples/platform-switching/web-assembly/host/glue/list.zig new file mode 100644 index 00000000000..c8fdcccaa9d --- /dev/null +++ b/examples/platform-switching/web-assembly/host/glue/list.zig @@ -0,0 +1,1035 @@ +const std = @import("std"); +const utils = @import("utils.zig"); +const UpdateMode = utils.UpdateMode; +const mem = std.mem; +const math = std.math; + +const expect = std.testing.expect; + +const EqFn = *const fn (?[*]u8, ?[*]u8) callconv(.C) bool; +const CompareFn = *const fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) u8; +const Opaque = ?[*]u8; + +const Inc = *const fn (?[*]u8) callconv(.C) void; +const IncN = *const fn (?[*]u8, usize) callconv(.C) void; +const Dec = *const fn (?[*]u8) callconv(.C) void; +const HasTagId = *const fn (u16, ?[*]u8) callconv(.C) extern struct { matched: bool, data: ?[*]u8 }; + +const SEAMLESS_SLICE_BIT: usize = + @as(usize, @bitCast(@as(isize, std.math.minInt(isize)))); + +pub const RocList = extern struct { + bytes: ?[*]u8, + length: usize, + // For normal lists, contains the capacity. + // For seamless slices contains the pointer to the original allocation. + // This pointer is to the first element of the original list. + // Note we storing an allocation pointer, the pointer must be right shifted by one. + capacity_or_alloc_ptr: usize, + + pub inline fn len(self: RocList) usize { + return self.length; + } + + pub fn getCapacity(self: RocList) usize { + const list_capacity = self.capacity_or_alloc_ptr; + const slice_capacity = self.length; + const slice_mask = self.seamlessSliceMask(); + const capacity = (list_capacity & ~slice_mask) | (slice_capacity & slice_mask); + return capacity; + } + + pub fn isSeamlessSlice(self: RocList) bool { + return @as(isize, @bitCast(self.capacity_or_alloc_ptr)) < 0; + } + + // This returns all ones if the list is a seamless slice. + // Otherwise, it returns all zeros. + // This is done without branching for optimization purposes. + pub fn seamlessSliceMask(self: RocList) usize { + return @as(usize, @bitCast(@as(isize, @bitCast(self.capacity_or_alloc_ptr)) >> (@bitSizeOf(isize) - 1))); + } + + pub fn isEmpty(self: RocList) bool { + return self.len() == 0; + } + + pub fn empty() RocList { + return RocList{ .bytes = null, .length = 0, .capacity_or_alloc_ptr = 0 }; + } + + pub fn eql(self: RocList, other: RocList) bool { + if (self.len() != other.len()) { + return false; + } + + // Their lengths are the same, and one is empty; they're both empty! + if (self.isEmpty()) { + return true; + } + + var index: usize = 0; + const self_bytes = self.bytes orelse unreachable; + const other_bytes = other.bytes orelse unreachable; + + while (index < self.len()) { + if (self_bytes[index] != other_bytes[index]) { + return false; + } + + index += 1; + } + + return true; + } + + pub fn fromSlice(comptime T: type, slice: []const T) RocList { + if (slice.len == 0) { + return RocList.empty(); + } + + var list = allocate(@alignOf(T), slice.len, @sizeOf(T)); + + if (slice.len > 0) { + const dest = list.bytes orelse unreachable; + const src = @as([*]const u8, @ptrCast(slice.ptr)); + const num_bytes = slice.len * @sizeOf(T); + + @memcpy(dest[0..num_bytes], src[0..num_bytes]); + } + + return list; + } + + // returns a pointer to the original allocation. + // This pointer points to the first element of the allocation. + // The pointer is to just after the refcount. + // For big lists, it just returns their bytes pointer. + // For seamless slices, it returns the pointer stored in capacity_or_alloc_ptr. + pub fn getAllocationPtr(self: RocList) ?[*]u8 { + const list_alloc_ptr = @intFromPtr(self.bytes); + const slice_alloc_ptr = self.capacity_or_alloc_ptr << 1; + const slice_mask = self.seamlessSliceMask(); + const alloc_ptr = (list_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + return @as(?[*]u8, @ptrFromInt(alloc_ptr)); + } + + pub fn decref(self: RocList, alignment: u32) void { + // We use the raw capacity to ensure we always decrement the refcount of seamless slices. + utils.decref(self.getAllocationPtr(), self.capacity_or_alloc_ptr, alignment); + } + + pub fn elements(self: RocList, comptime T: type) ?[*]T { + return @as(?[*]T, @ptrCast(@alignCast(self.bytes))); + } + + pub fn isUnique(self: RocList) bool { + return self.refcountMachine() == utils.REFCOUNT_ONE; + } + + fn refcountMachine(self: RocList) usize { + if (self.getCapacity() == 0 and !self.isSeamlessSlice()) { + // the zero-capacity is Clone, copying it will not leak memory + return utils.REFCOUNT_ONE; + } + + const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.bytes))); + return (ptr - 1)[0]; + } + + fn refcountHuman(self: RocList) usize { + return self.refcountMachine() - utils.REFCOUNT_ONE + 1; + } + + pub fn makeUniqueExtra(self: RocList, alignment: u32, element_width: usize, update_mode: UpdateMode) RocList { + if (update_mode == .InPlace) { + return self; + } else { + return self.makeUnique(alignment, element_width); + } + } + + pub fn makeUnique(self: RocList, alignment: u32, element_width: usize) RocList { + if (self.isUnique()) { + return self; + } + + if (self.isEmpty()) { + // Empty is not necessarily unique on it's own. + // The list could have capacity and be shared. + self.decref(alignment); + return RocList.empty(); + } + + // unfortunately, we have to clone + var new_list = RocList.allocate(alignment, self.length, element_width); + + var old_bytes: [*]u8 = @as([*]u8, @ptrCast(self.bytes)); + var new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_list.bytes)); + + const number_of_bytes = self.len() * element_width; + @memcpy(new_bytes[0..number_of_bytes], old_bytes[0..number_of_bytes]); + + // NOTE we fuse an increment of all keys/values with a decrement of the input list. + self.decref(alignment); + + return new_list; + } + + pub fn allocate( + alignment: u32, + length: usize, + element_width: usize, + ) RocList { + if (length == 0) { + return empty(); + } + + const capacity = utils.calculateCapacity(0, length, element_width); + const data_bytes = capacity * element_width; + return RocList{ + .bytes = utils.allocateWithRefcount(data_bytes, alignment), + .length = length, + .capacity_or_alloc_ptr = capacity, + }; + } + + pub fn allocateExact( + alignment: u32, + length: usize, + element_width: usize, + ) RocList { + if (length == 0) { + return empty(); + } + + const data_bytes = length * element_width; + return RocList{ + .bytes = utils.allocateWithRefcount(data_bytes, alignment), + .length = length, + .capacity_or_alloc_ptr = length, + }; + } + + pub fn reallocate( + self: RocList, + alignment: u32, + new_length: usize, + element_width: usize, + ) RocList { + if (self.bytes) |source_ptr| { + if (self.isUnique() and !self.isSeamlessSlice()) { + const capacity = self.capacity_or_alloc_ptr; + if (capacity >= new_length) { + return RocList{ .bytes = self.bytes, .length = new_length, .capacity_or_alloc_ptr = capacity }; + } else { + const new_capacity = utils.calculateCapacity(capacity, new_length, element_width); + const new_source = utils.unsafeReallocate(source_ptr, alignment, capacity, new_capacity, element_width); + return RocList{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity }; + } + } + return self.reallocateFresh(alignment, new_length, element_width); + } + return RocList.allocate(alignment, new_length, element_width); + } + + /// reallocate by explicitly making a new allocation and copying elements over + fn reallocateFresh( + self: RocList, + alignment: u32, + new_length: usize, + element_width: usize, + ) RocList { + const old_length = self.length; + + const result = RocList.allocate(alignment, new_length, element_width); + + // transfer the memory + if (self.bytes) |source_ptr| { + const dest_ptr = result.bytes orelse unreachable; + + @memcpy(dest_ptr[0..(old_length * element_width)], source_ptr[0..(old_length * element_width)]); + @memset(dest_ptr[(old_length * element_width)..(new_length * element_width)], 0); + } + + self.decref(alignment); + + return result; + } +}; + +const Caller0 = *const fn (?[*]u8, ?[*]u8) callconv(.C) void; +const Caller1 = *const fn (?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller2 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller3 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; +const Caller4 = *const fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void; + +pub fn listMap( + list: RocList, + caller: Caller1, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + old_element_width: usize, + new_element_width: usize, +) callconv(.C) RocList { + if (list.bytes) |source_ptr| { + const size = list.len(); + var i: usize = 0; + const output = RocList.allocate(alignment, size, new_element_width); + const target_ptr = output.bytes orelse unreachable; + + if (data_is_owned) { + inc_n_data(data, size); + } + + while (i < size) : (i += 1) { + caller(data, source_ptr + (i * old_element_width), target_ptr + (i * new_element_width)); + } + + return output; + } else { + return RocList.empty(); + } +} + +fn decrementTail(list: RocList, start_index: usize, element_width: usize, dec: Dec) void { + if (list.bytes) |source| { + var i = start_index; + while (i < list.len()) : (i += 1) { + const element = source + i * element_width; + dec(element); + } + } +} + +pub fn listMap2( + list1: RocList, + list2: RocList, + caller: Caller2, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + dec_a: Dec, + dec_b: Dec, +) callconv(.C) RocList { + const output_length = @min(list1.len(), list2.len()); + + // if the lists don't have equal length, we must consume the remaining elements + // In this case we consume by (recursively) decrementing the elements + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + const output = RocList.allocate(alignment, output_length, c_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const target = target_ptr + i * c_width; + caller(data, element_a, element_b, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listMap3( + list1: RocList, + list2: RocList, + list3: RocList, + caller: Caller3, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + d_width: usize, + dec_a: Dec, + dec_b: Dec, + dec_c: Dec, +) callconv(.C) RocList { + const smaller_length = @min(list1.len(), list2.len()); + const output_length = @min(smaller_length, list3.len()); + + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + decrementTail(list3, output_length, c_width, dec_c); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + if (list3.bytes) |source_c| { + const output = RocList.allocate(alignment, output_length, d_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const element_c = source_c + i * c_width; + const target = target_ptr + i * d_width; + + caller(data, element_a, element_b, element_c, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listMap4( + list1: RocList, + list2: RocList, + list3: RocList, + list4: RocList, + caller: Caller4, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + a_width: usize, + b_width: usize, + c_width: usize, + d_width: usize, + e_width: usize, + dec_a: Dec, + dec_b: Dec, + dec_c: Dec, + dec_d: Dec, +) callconv(.C) RocList { + const output_length = @min(@min(list1.len(), list2.len()), @min(list3.len(), list4.len())); + + decrementTail(list1, output_length, a_width, dec_a); + decrementTail(list2, output_length, b_width, dec_b); + decrementTail(list3, output_length, c_width, dec_c); + decrementTail(list4, output_length, d_width, dec_d); + + if (data_is_owned) { + inc_n_data(data, output_length); + } + + if (list1.bytes) |source_a| { + if (list2.bytes) |source_b| { + if (list3.bytes) |source_c| { + if (list4.bytes) |source_d| { + const output = RocList.allocate(alignment, output_length, e_width); + const target_ptr = output.bytes orelse unreachable; + + var i: usize = 0; + while (i < output_length) : (i += 1) { + const element_a = source_a + i * a_width; + const element_b = source_b + i * b_width; + const element_c = source_c + i * c_width; + const element_d = source_d + i * d_width; + const target = target_ptr + i * e_width; + + caller(data, element_a, element_b, element_c, element_d, target); + } + + return output; + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } + } else { + return RocList.empty(); + } +} + +pub fn listWithCapacity( + capacity: u64, + alignment: u32, + element_width: usize, +) callconv(.C) RocList { + return listReserve(RocList.empty(), alignment, capacity, element_width, .InPlace); +} + +pub fn listReserve( + list: RocList, + alignment: u32, + spare: u64, + element_width: usize, + update_mode: UpdateMode, +) callconv(.C) RocList { + const original_len = list.len(); + const cap = @as(u64, @intCast(list.getCapacity())); + const desired_cap = @as(u64, @intCast(original_len)) +| spare; + + if ((update_mode == .InPlace or list.isUnique()) and cap >= desired_cap) { + return list; + } else { + // Make sure on 32-bit targets we don't accidentally wrap when we cast our U64 desired capacity to U32. + const reserve_size: u64 = @min(desired_cap, @as(u64, @intCast(std.math.maxInt(usize)))); + + var output = list.reallocate(alignment, @as(usize, @intCast(reserve_size)), element_width); + output.length = original_len; + return output; + } +} + +pub fn listReleaseExcessCapacity( + list: RocList, + alignment: u32, + element_width: usize, + update_mode: UpdateMode, +) callconv(.C) RocList { + const old_length = list.len(); + // We use the direct list.capacity_or_alloc_ptr to make sure both that there is no extra capacity and that it isn't a seamless slice. + if ((update_mode == .InPlace or list.isUnique()) and list.capacity_or_alloc_ptr == old_length) { + return list; + } else if (old_length == 0) { + list.decref(alignment); + return RocList.empty(); + } else { + var output = RocList.allocateExact(alignment, old_length, element_width); + if (list.bytes) |source_ptr| { + const dest_ptr = output.bytes orelse unreachable; + + @memcpy(dest_ptr[0..(old_length * element_width)], source_ptr[0..(old_length * element_width)]); + } + list.decref(alignment); + return output; + } +} + +pub fn listAppendUnsafe( + list: RocList, + element: Opaque, + element_width: usize, +) callconv(.C) RocList { + const old_length = list.len(); + var output = list; + output.length += 1; + + if (output.bytes) |bytes| { + if (element) |source| { + const target = bytes + old_length * element_width; + @memcpy(target[0..element_width], source[0..element_width]); + } + } + + return output; +} + +fn listAppend(list: RocList, alignment: u32, element: Opaque, element_width: usize, update_mode: UpdateMode) callconv(.C) RocList { + const with_capacity = listReserve(list, alignment, 1, element_width, update_mode); + return listAppendUnsafe(with_capacity, element, element_width); +} + +pub fn listPrepend(list: RocList, alignment: u32, element: Opaque, element_width: usize) callconv(.C) RocList { + const old_length = list.len(); + // TODO: properly wire in update mode. + var with_capacity = listReserve(list, alignment, 1, element_width, .Immutable); + with_capacity.length += 1; + + // can't use one memcpy here because source and target overlap + if (with_capacity.bytes) |target| { + var i: usize = old_length; + + while (i > 0) { + i -= 1; + + // move the ith element to the (i + 1)th position + const to = target + (i + 1) * element_width; + const from = target + i * element_width; + @memcpy(to[0..element_width], from[0..element_width]); + } + + // finally copy in the new first element + if (element) |source| { + @memcpy(target[0..element_width], source[0..element_width]); + } + } + + return with_capacity; +} + +pub fn listSwap( + list: RocList, + alignment: u32, + element_width: usize, + index_1: u64, + index_2: u64, + update_mode: UpdateMode, +) callconv(.C) RocList { + const size = @as(u64, @intCast(list.len())); + if (index_1 == index_2 or index_1 >= size or index_2 >= size) { + // Either one index was out of bounds, or both indices were the same; just return + return list; + } + + const newList = blk: { + if (update_mode == .InPlace) { + break :blk list; + } else { + break :blk list.makeUnique(alignment, element_width); + } + }; + + const source_ptr = @as([*]u8, @ptrCast(newList.bytes)); + + swapElements(source_ptr, element_width, @as(usize, + // We already verified that both indices are less than the stored list length, + // which is usize, so casting them to usize will definitely be lossless. + @intCast(index_1)), @as(usize, @intCast(index_2))); + + return newList; +} + +pub fn listSublist( + list: RocList, + alignment: u32, + element_width: usize, + start_u64: u64, + len_u64: u64, + dec: Dec, +) callconv(.C) RocList { + const size = list.len(); + if (size == 0 or start_u64 >= @as(u64, @intCast(size))) { + // Decrement the reference counts of all elements. + if (list.bytes) |source_ptr| { + var i: usize = 0; + while (i < size) : (i += 1) { + const element = source_ptr + i * element_width; + dec(element); + } + } + if (list.isUnique()) { + var output = list; + output.length = 0; + return output; + } + list.decref(alignment); + return RocList.empty(); + } + + if (list.bytes) |source_ptr| { + // This cast is lossless because we would have early-returned already + // if `start_u64` were greater than `size`, and `size` fits in usize. + const start: usize = @intCast(start_u64); + const drop_start_len = start; + + // (size - start) can't overflow because we would have early-returned already + // if `start` were greater than `size`. + const size_minus_start = size - start; + + // This outer cast to usize is lossless. size, start, and size_minus_start all fit in usize, + // and @min guarantees that if `len_u64` gets returned, it's because it was smaller + // than something that fit in usize. + const keep_len = @as(usize, @intCast(@min(len_u64, @as(u64, @intCast(size_minus_start))))); + + // This can't overflow because if len > size_minus_start, + // then keep_len == size_minus_start and this will be 0. + // Alternatively, if len <= size_minus_start, then keep_len will + // be equal to len, meaning keep_len <= size_minus_start too, + // which in turn means this won't overflow. + const drop_end_len = size_minus_start - keep_len; + + // Decrement the reference counts of elements before `start`. + var i: usize = 0; + while (i < drop_start_len) : (i += 1) { + const element = source_ptr + i * element_width; + dec(element); + } + + // Decrement the reference counts of elements after `start + keep_len`. + i = 0; + while (i < drop_end_len) : (i += 1) { + const element = source_ptr + (start + keep_len + i) * element_width; + dec(element); + } + + if (start == 0 and list.isUnique()) { + var output = list; + output.length = keep_len; + return output; + } else { + const list_alloc_ptr = (@intFromPtr(source_ptr) >> 1) | SEAMLESS_SLICE_BIT; + const slice_alloc_ptr = list.capacity_or_alloc_ptr; + const slice_mask = list.seamlessSliceMask(); + const alloc_ptr = (list_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + return RocList{ + .bytes = source_ptr + start * element_width, + .length = keep_len, + .capacity_or_alloc_ptr = alloc_ptr, + }; + } + } + + return RocList.empty(); +} + +pub fn listDropAt( + list: RocList, + alignment: u32, + element_width: usize, + drop_index_u64: u64, + dec: Dec, +) callconv(.C) RocList { + const size = list.len(); + const size_u64 = @as(u64, @intCast(size)); + // If droping the first or last element, return a seamless slice. + // For simplicity, do this by calling listSublist. + // In the future, we can test if it is faster to manually inline the important parts here. + if (drop_index_u64 == 0) { + return listSublist(list, alignment, element_width, 1, size -| 1, dec); + } else if (drop_index_u64 == size_u64 - 1) { // It's fine if (size - 1) wraps on size == 0 here, + // because if size is 0 then it's always fine for this branch to be taken; no + // matter what drop_index was, we're size == 0, so empty list will always be returned. + return listSublist(list, alignment, element_width, 0, size -| 1, dec); + } + + if (list.bytes) |source_ptr| { + if (drop_index_u64 >= size_u64) { + return list; + } + + // This cast must be lossless, because we would have just early-returned if drop_index + // were >= than `size`, and we know `size` fits in usize. + const drop_index: usize = @intCast(drop_index_u64); + + const element = source_ptr + drop_index * element_width; + dec(element); + + // NOTE + // we need to return an empty list explicitly, + // because we rely on the pointer field being null if the list is empty + // which also requires duplicating the utils.decref call to spend the RC token + if (size < 2) { + list.decref(alignment); + return RocList.empty(); + } + + if (list.isUnique()) { + var i = drop_index; + while (i < size - 1) : (i += 1) { + const copy_target = source_ptr + i * element_width; + const copy_source = copy_target + element_width; + + @memcpy(copy_target[0..element_width], copy_source[0..element_width]); + } + + var new_list = list; + + new_list.length -= 1; + return new_list; + } + + const output = RocList.allocate(alignment, size - 1, element_width); + const target_ptr = output.bytes orelse unreachable; + + const head_size = drop_index * element_width; + @memcpy(target_ptr[0..head_size], source_ptr[0..head_size]); + + const tail_target = target_ptr + drop_index * element_width; + const tail_source = source_ptr + (drop_index + 1) * element_width; + const tail_size = (size - drop_index - 1) * element_width; + @memcpy(tail_target[0..tail_size], tail_source[0..tail_size]); + + list.decref(alignment); + + return output; + } else { + return RocList.empty(); + } +} + +fn partition(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) isize { + const pivot = source_ptr + (@as(usize, @intCast(high)) * element_width); + var i = (low - 1); // Index of smaller element and indicates the right position of pivot found so far + var j = low; + + while (j <= high - 1) : (j += 1) { + const current_elem = source_ptr + (@as(usize, @intCast(j)) * element_width); + + const ordering = wrapper(transform, current_elem, pivot); + const order = @as(utils.Ordering, @enumFromInt(ordering)); + + switch (order) { + utils.Ordering.LT => { + // the current element is smaller than the pivot; swap it + i += 1; + swapElements(source_ptr, element_width, @as(usize, @intCast(i)), @as(usize, @intCast(j))); + }, + utils.Ordering.EQ, utils.Ordering.GT => {}, + } + } + swapElements(source_ptr, element_width, @as(usize, @intCast(i + 1)), @as(usize, @intCast(high))); + return (i + 1); +} + +fn quicksort(source_ptr: [*]u8, transform: Opaque, wrapper: CompareFn, element_width: usize, low: isize, high: isize) void { + if (low < high) { + // partition index + const pi = partition(source_ptr, transform, wrapper, element_width, low, high); + + _ = quicksort(source_ptr, transform, wrapper, element_width, low, pi - 1); // before pi + _ = quicksort(source_ptr, transform, wrapper, element_width, pi + 1, high); // after pi + } +} + +pub fn listSortWith( + input: RocList, + caller: CompareFn, + data: Opaque, + inc_n_data: IncN, + data_is_owned: bool, + alignment: u32, + element_width: usize, +) callconv(.C) RocList { + var list = input.makeUnique(alignment, element_width); + + if (data_is_owned) { + inc_n_data(data, list.len()); + } + + if (list.bytes) |source_ptr| { + const low = 0; + const high: isize = @as(isize, @intCast(list.len())) - 1; + quicksort(source_ptr, data, caller, element_width, low, high); + } + + return list; +} + +// SWAP ELEMENTS + +inline fn swapHelp(width: usize, temporary: [*]u8, ptr1: [*]u8, ptr2: [*]u8) void { + @memcpy(temporary[0..width], ptr1[0..width]); + @memcpy(ptr1[0..width], ptr2[0..width]); + @memcpy(ptr2[0..width], temporary[0..width]); +} + +fn swap(width_initial: usize, p1: [*]u8, p2: [*]u8) void { + const threshold: usize = 64; + + var width = width_initial; + + var ptr1 = p1; + var ptr2 = p2; + + var buffer_actual: [threshold]u8 = undefined; + var buffer: [*]u8 = buffer_actual[0..]; + + while (true) { + if (width < threshold) { + swapHelp(width, buffer, ptr1, ptr2); + return; + } else { + swapHelp(threshold, buffer, ptr1, ptr2); + + ptr1 += threshold; + ptr2 += threshold; + + width -= threshold; + } + } +} + +fn swapElements(source_ptr: [*]u8, element_width: usize, index_1: usize, index_2: usize) void { + var element_at_i = source_ptr + (index_1 * element_width); + var element_at_j = source_ptr + (index_2 * element_width); + + return swap(element_width, element_at_i, element_at_j); +} + +pub fn listConcat(list_a: RocList, list_b: RocList, alignment: u32, element_width: usize) callconv(.C) RocList { + // NOTE we always use list_a! because it is owned, we must consume it, and it may have unused capacity + if (list_b.isEmpty()) { + if (list_a.getCapacity() == 0) { + // a could be a seamless slice, so we still need to decref. + list_a.decref(alignment); + return list_b; + } else { + // we must consume this list. Even though it has no elements, it could still have capacity + list_b.decref(alignment); + + return list_a; + } + } else if (list_a.isUnique()) { + const total_length: usize = list_a.len() + list_b.len(); + + const resized_list_a = list_a.reallocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const source_a = resized_list_a.bytes orelse unreachable; + const source_b = list_b.bytes orelse unreachable; + @memcpy(source_a[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]); + + // decrement list b. + list_b.decref(alignment); + + return resized_list_a; + } else if (list_b.isUnique()) { + const total_length: usize = list_a.len() + list_b.len(); + + const resized_list_b = list_b.reallocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const source_a = list_a.bytes orelse unreachable; + const source_b = resized_list_b.bytes orelse unreachable; + + // This is a bit special, we need to first copy the elements of list_b to the end, + // then copy the elements of list_a to the beginning. + // This first call must use mem.copy because the slices might overlap. + const byte_count_a = list_a.len() * element_width; + const byte_count_b = list_b.len() * element_width; + mem.copyBackwards(u8, source_b[byte_count_a .. byte_count_a + byte_count_b], source_b[0..byte_count_b]); + @memcpy(source_b[0..byte_count_a], source_a[0..byte_count_a]); + + // decrement list a. + list_a.decref(alignment); + + return resized_list_b; + } + const total_length: usize = list_a.len() + list_b.len(); + + const output = RocList.allocate(alignment, total_length, element_width); + + // These must exist, otherwise, the lists would have been empty. + const target = output.bytes orelse unreachable; + const source_a = list_a.bytes orelse unreachable; + const source_b = list_b.bytes orelse unreachable; + + @memcpy(target[0..(list_a.len() * element_width)], source_a[0..(list_a.len() * element_width)]); + @memcpy(target[(list_a.len() * element_width)..(total_length * element_width)], source_b[0..(list_b.len() * element_width)]); + + // decrement list a and b. + list_a.decref(alignment); + list_b.decref(alignment); + + return output; +} + +pub fn listReplaceInPlace( + list: RocList, + index: u64, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) callconv(.C) RocList { + // INVARIANT: bounds checking happens on the roc side + // + // at the time of writing, the function is implemented roughly as + // `if inBounds then LowLevelListReplace input index item else input` + // so we don't do a bounds check here. Hence, the list is also non-empty, + // because inserting into an empty list is always out of bounds, + // and it's always safe to cast index to usize. + return listReplaceInPlaceHelp(list, @as(usize, @intCast(index)), element, element_width, out_element); +} + +pub fn listReplace( + list: RocList, + alignment: u32, + index: u64, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) callconv(.C) RocList { + // INVARIANT: bounds checking happens on the roc side + // + // at the time of writing, the function is implemented roughly as + // `if inBounds then LowLevelListReplace input index item else input` + // so we don't do a bounds check here. Hence, the list is also non-empty, + // because inserting into an empty list is always out of bounds, + // and it's always safe to cast index to usize. + return listReplaceInPlaceHelp(list.makeUnique(alignment, element_width), @as(usize, @intCast(index)), element, element_width, out_element); +} + +inline fn listReplaceInPlaceHelp( + list: RocList, + index: usize, + element: Opaque, + element_width: usize, + out_element: ?[*]u8, +) RocList { + // the element we will replace + var element_at_index = (list.bytes orelse unreachable) + (index * element_width); + + // copy out the old element + @memcpy((out_element orelse unreachable)[0..element_width], element_at_index[0..element_width]); + + // copy in the new element + @memcpy(element_at_index[0..element_width], (element orelse unreachable)[0..element_width]); + + return list; +} + +pub fn listIsUnique( + list: RocList, +) callconv(.C) bool { + return list.isEmpty() or list.isUnique(); +} + +pub fn listClone( + list: RocList, + alignment: u32, + element_width: usize, +) callconv(.C) RocList { + return list.makeUnique(alignment, element_width); +} + +pub fn listCapacity( + list: RocList, +) callconv(.C) usize { + return list.getCapacity(); +} + +pub fn listAllocationPtr( + list: RocList, +) callconv(.C) ?[*]u8 { + return list.getAllocationPtr(); +} + +test "listConcat: non-unique with unique overlapping" { + var nonUnique = RocList.fromSlice(u8, ([_]u8{1})[0..]); + var bytes: [*]u8 = @as([*]u8, @ptrCast(nonUnique.bytes)); + const ptr_width = @sizeOf(usize); + const refcount_ptr = @as([*]isize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(bytes)) - ptr_width)); + utils.increfRcPtrC(&refcount_ptr[0], 1); + defer nonUnique.decref(@sizeOf(u8)); // listConcat will dec the other refcount + + var unique = RocList.fromSlice(u8, ([_]u8{ 2, 3, 4 })[0..]); + defer unique.decref(@sizeOf(u8)); + + var concatted = listConcat(nonUnique, unique, 1, 1); + var wanted = RocList.fromSlice(u8, ([_]u8{ 1, 2, 3, 4 })[0..]); + defer wanted.decref(@sizeOf(u8)); + + try expect(concatted.eql(wanted)); +} diff --git a/examples/platform-switching/web-assembly/host/glue/main.zig b/examples/platform-switching/web-assembly/host/glue/main.zig new file mode 100644 index 00000000000..d95eb652d08 --- /dev/null +++ b/examples/platform-switching/web-assembly/host/glue/main.zig @@ -0,0 +1,3 @@ +// ⚠️ GENERATED CODE ⚠️ +// +// This package is generated by the `roc glue` CLI command \ No newline at end of file diff --git a/examples/platform-switching/web-assembly/host/glue/str.zig b/examples/platform-switching/web-assembly/host/glue/str.zig new file mode 100644 index 00000000000..4b8cc894c36 --- /dev/null +++ b/examples/platform-switching/web-assembly/host/glue/str.zig @@ -0,0 +1,2392 @@ +const utils = @import("utils.zig"); +const RocList = @import("list.zig").RocList; +const UpdateMode = utils.UpdateMode; +const std = @import("std"); +const mem = std.mem; +const unicode = std.unicode; +const testing = std.testing; +const expectEqual = testing.expectEqual; +const expectError = testing.expectError; +const expect = testing.expect; + +const InPlace = enum(u8) { + InPlace, + Clone, +}; + +const MASK_ISIZE: isize = std.math.minInt(isize); +const MASK: usize = @as(usize, @bitCast(MASK_ISIZE)); +const SEAMLESS_SLICE_BIT: usize = MASK; + +const SMALL_STR_MAX_LENGTH = SMALL_STRING_SIZE - 1; +const SMALL_STRING_SIZE = @sizeOf(RocStr); + +fn init_blank_small_string(comptime n: usize) [n]u8 { + var prime_list: [n]u8 = undefined; + + var i = 0; + while (i < n) : (i += 1) { + prime_list[i] = 0; + } + + return prime_list; +} + +pub const RocStr = extern struct { + bytes: ?[*]u8, + length: usize, + // For big strs, contains the capacity. + // For seamless slices contains the pointer to the original allocation. + // This pointer is to the first character of the original string. + // Note we storing an allocation pointer, the pointer must be right shifted by one. + capacity_or_alloc_ptr: usize, + + pub const alignment = @alignOf(usize); + + pub inline fn empty() RocStr { + return RocStr{ + .length = 0, + .bytes = null, + .capacity_or_alloc_ptr = MASK, + }; + } + + // This clones the pointed-to bytes if they won't fit in a + // small string, and returns a (pointer, len) tuple which points to them. + pub fn init(bytes_ptr: [*]const u8, length: usize) RocStr { + var result = RocStr.allocate(length); + @memcpy(result.asU8ptrMut()[0..length], bytes_ptr[0..length]); + + return result; + } + + // This requires that the list is non-null. + // It also requires that start and count define a slice that does not go outside the bounds of the list. + pub fn fromSubListUnsafe(list: RocList, start: usize, count: usize, update_mode: UpdateMode) RocStr { + const start_byte = @as([*]u8, @ptrCast(list.bytes)) + start; + if (list.isSeamlessSlice()) { + return RocStr{ + .bytes = start_byte, + .length = count | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = list.capacity_or_alloc_ptr & (~SEAMLESS_SLICE_BIT), + }; + } else if (start == 0 and (update_mode == .InPlace or list.isUnique())) { + // Rare case, we can take over the original list. + return RocStr{ + .bytes = start_byte, + .length = count, + .capacity_or_alloc_ptr = list.capacity_or_alloc_ptr, // This is guaranteed to be a proper capacity. + }; + } else { + // Create seamless slice pointing to the list. + return RocStr{ + .bytes = start_byte, + .length = count | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = @intFromPtr(list.bytes) >> 1, + }; + } + } + + pub fn isSeamlessSlice(self: RocStr) bool { + return !self.isSmallStr() and @as(isize, @bitCast(self.length)) < 0; + } + + pub fn fromSlice(slice: []const u8) RocStr { + return RocStr.init(slice.ptr, slice.len); + } + + fn allocateBig(length: usize, capacity: usize) RocStr { + const first_element = utils.allocateWithRefcount(capacity, @sizeOf(usize)); + + return RocStr{ + .bytes = first_element, + .length = length, + .capacity_or_alloc_ptr = capacity, + }; + } + + // allocate space for a (big or small) RocStr, but put nothing in it yet. + // May have a larger capacity than the length. + pub fn allocate(length: usize) RocStr { + const element_width = 1; + const result_is_big = length >= SMALL_STRING_SIZE; + + if (result_is_big) { + const capacity = utils.calculateCapacity(0, length, element_width); + return RocStr.allocateBig(length, capacity); + } else { + var string = RocStr.empty(); + + string.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + + return string; + } + } + + // allocate space for a (big or small) RocStr, but put nothing in it yet. + // Will have the exact same capacity as length if it is not a small string. + pub fn allocateExact(length: usize) RocStr { + const result_is_big = length >= SMALL_STRING_SIZE; + + if (result_is_big) { + return RocStr.allocateBig(length, length); + } else { + var string = RocStr.empty(); + + string.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + + return string; + } + } + + // This returns all ones if the list is a seamless slice. + // Otherwise, it returns all zeros. + // This is done without branching for optimization purposes. + pub fn seamlessSliceMask(self: RocStr) usize { + return @as(usize, @bitCast(@as(isize, @bitCast(self.length)) >> (@bitSizeOf(isize) - 1))); + } + + // returns a pointer to the original allocation. + // This pointer points to the first element of the allocation. + // The pointer is to just after the refcount. + // For big strings, it just returns their bytes pointer. + // For seamless slices, it returns the pointer stored in capacity_or_alloc_ptr. + // This does not return a valid value if the input is a small string. + pub fn getAllocationPtr(self: RocStr) ?[*]u8 { + const str_alloc_ptr = @intFromPtr(self.bytes); + const slice_alloc_ptr = self.capacity_or_alloc_ptr << 1; + const slice_mask = self.seamlessSliceMask(); + const alloc_ptr = (str_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + return @as(?[*]u8, @ptrFromInt(alloc_ptr)); + } + + pub fn incref(self: RocStr, n: usize) void { + if (!self.isSmallStr()) { + const alloc_ptr = self.getAllocationPtr(); + if (alloc_ptr != null) { + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(alloc_ptr))); + utils.increfRcPtrC(@as(*isize, @ptrCast(isizes - 1)), @as(isize, @intCast(n))); + } + } + } + + pub fn decref(self: RocStr) void { + if (!self.isSmallStr()) { + utils.decref(self.getAllocationPtr(), self.capacity_or_alloc_ptr, RocStr.alignment); + } + } + + pub fn eq(self: RocStr, other: RocStr) bool { + // If they are byte-for-byte equal, they're definitely equal! + if (self.bytes == other.bytes and self.length == other.length and self.capacity_or_alloc_ptr == other.capacity_or_alloc_ptr) { + return true; + } + + const self_len = self.len(); + const other_len = other.len(); + + // If their lengths are different, they're definitely unequal. + if (self_len != other_len) { + return false; + } + + // Now we have to look at the string contents + const self_bytes = self.asU8ptr(); + const other_bytes = other.asU8ptr(); + // TODO: we can make an optimization like memcmp does in glibc. + // We can check the min shared alignment 1, 2, 4, or 8. + // Then do a copy at that alignment before falling back on one byte at a time. + // Currently we have to be unaligned because slices can be at any alignment. + var b: usize = 0; + while (b < self_len) : (b += 1) { + if (self_bytes[b] != other_bytes[b]) { + return false; + } + } + + return true; + } + + pub fn clone(str: RocStr) RocStr { + if (str.isSmallStr()) { + // just return the bytes + return str; + } else { + var new_str = RocStr.allocateBig(str.length, str.length); + + var old_bytes: [*]u8 = @as([*]u8, @ptrCast(str.bytes)); + var new_bytes: [*]u8 = @as([*]u8, @ptrCast(new_str.bytes)); + + @memcpy(new_bytes[0..str.length], old_bytes[0..str.length]); + + return new_str; + } + } + + pub fn reallocate( + self: RocStr, + new_length: usize, + ) RocStr { + const element_width = 1; + const old_capacity = self.getCapacity(); + + if (self.isSmallStr() or self.isSeamlessSlice() or !self.isUnique()) { + return self.reallocateFresh(new_length); + } + + if (self.bytes) |source_ptr| { + if (old_capacity > new_length) { + var output = self; + output.setLen(new_length); + return output; + } + const new_capacity = utils.calculateCapacity(old_capacity, new_length, element_width); + const new_source = utils.unsafeReallocate( + source_ptr, + RocStr.alignment, + old_capacity, + new_capacity, + element_width, + ); + + return RocStr{ .bytes = new_source, .length = new_length, .capacity_or_alloc_ptr = new_capacity }; + } + return self.reallocateFresh(new_length); + } + + /// reallocate by explicitly making a new allocation and copying elements over + fn reallocateFresh( + self: RocStr, + new_length: usize, + ) RocStr { + const old_length = self.len(); + + const element_width = 1; + const result_is_big = new_length >= SMALL_STRING_SIZE; + + if (result_is_big) { + const capacity = utils.calculateCapacity(0, new_length, element_width); + var result = RocStr.allocateBig(new_length, capacity); + + // transfer the memory + + const source_ptr = self.asU8ptr(); + const dest_ptr = result.asU8ptrMut(); + + std.mem.copy(u8, dest_ptr[0..old_length], source_ptr[0..old_length]); + @memset(dest_ptr[old_length..new_length], 0); + + self.decref(); + + return result; + } else { + var string = RocStr.empty(); + + // I believe taking this reference on the stack here is important for correctness. + // Doing it via a method call seemed to cause issues + const dest_ptr = @as([*]u8, @ptrCast(&string)); + dest_ptr[@sizeOf(RocStr) - 1] = @as(u8, @intCast(new_length)) | 0b1000_0000; + + const source_ptr = self.asU8ptr(); + + std.mem.copy(u8, dest_ptr[0..old_length], source_ptr[0..old_length]); + @memset(dest_ptr[old_length..new_length], 0); + + self.decref(); + + return string; + } + } + + pub fn isSmallStr(self: RocStr) bool { + return @as(isize, @bitCast(self.capacity_or_alloc_ptr)) < 0; + } + + test "isSmallStr: returns true for empty string" { + try expect(isSmallStr(RocStr.empty())); + } + + fn asArray(self: RocStr) [@sizeOf(RocStr)]u8 { + const as_ptr = @as([*]const u8, @ptrCast(&self)); + const slice = as_ptr[0..@sizeOf(RocStr)]; + + return slice.*; + } + + pub fn len(self: RocStr) usize { + if (self.isSmallStr()) { + return self.asArray()[@sizeOf(RocStr) - 1] ^ 0b1000_0000; + } else { + return self.length & (~SEAMLESS_SLICE_BIT); + } + } + + pub fn setLen(self: *RocStr, length: usize) void { + if (self.isSmallStr()) { + self.asU8ptrMut()[@sizeOf(RocStr) - 1] = @as(u8, @intCast(length)) | 0b1000_0000; + } else { + self.length = length | (SEAMLESS_SLICE_BIT & self.length); + } + } + + pub fn getCapacity(self: RocStr) usize { + if (self.isSmallStr()) { + return SMALL_STR_MAX_LENGTH; + } else if (self.isSeamlessSlice()) { + return self.length & (~SEAMLESS_SLICE_BIT); + } else { + return self.capacity_or_alloc_ptr; + } + } + + // This does a small string check, but no bounds checking whatsoever! + pub fn getUnchecked(self: RocStr, index: usize) u8 { + if (self.isSmallStr()) { + return self.asArray()[index]; + } else { + const bytes = self.bytes orelse unreachable; + + return bytes[index]; + } + } + + pub fn isEmpty(self: RocStr) bool { + return self.len() == 0; + } + + pub fn isUnique(self: RocStr) bool { + // small strings can be copied + if (self.isSmallStr()) { + return true; + } + + // otherwise, check if the refcount is one + return @call(.always_inline, RocStr.isRefcountOne, .{self}); + } + + fn isRefcountOne(self: RocStr) bool { + return self.refcountMachine() == utils.REFCOUNT_ONE; + } + + fn refcountMachine(self: RocStr) usize { + if ((self.getCapacity() == 0 and !self.isSeamlessSlice()) or self.isSmallStr()) { + return utils.REFCOUNT_ONE; + } + + const ptr: [*]usize = @as([*]usize, @ptrCast(@alignCast(self.bytes))); + return (ptr - 1)[0]; + } + + fn refcountHuman(self: RocStr) usize { + return self.refcountMachine() - utils.REFCOUNT_ONE + 1; + } + + pub fn asSlice(self: *const RocStr) []const u8 { + return self.asU8ptr()[0..self.len()]; + } + + pub fn asSliceWithCapacity(self: *const RocStr) []const u8 { + return self.asU8ptr()[0..self.getCapacity()]; + } + + pub fn asSliceWithCapacityMut(self: *RocStr) []u8 { + return self.asU8ptrMut()[0..self.getCapacity()]; + } + + pub fn asU8ptr(self: *const RocStr) [*]const u8 { + if (self.isSmallStr()) { + return @as([*]const u8, @ptrCast(self)); + } else { + return @as([*]const u8, @ptrCast(self.bytes)); + } + } + + pub fn asU8ptrMut(self: *RocStr) [*]u8 { + if (self.isSmallStr()) { + return @as([*]u8, @ptrCast(self)); + } else { + return @as([*]u8, @ptrCast(self.bytes)); + } + } + + // Given a pointer to some bytes, write the first (len) bytes of this + // RocStr's contents into it. + // + // One use for this function is writing into an `alloca` for a C string that + // only needs to live long enough to be passed as an argument to + // a C function - like the file path argument to `fopen`. + pub fn memcpy(self: RocStr, dest: [*]u8) void { + const src = self.asU8ptr(); + @memcpy(dest[0..self.len()], src[0..self.len()]); + } + + test "RocStr.eq: small, equal" { + const str1_len = 3; + var str1: [str1_len]u8 = "abc".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + try expect(roc_str1.eq(roc_str2)); + + roc_str1.decref(); + roc_str2.decref(); + } + + test "RocStr.eq: small, not equal, different length" { + const str1_len = 4; + var str1: [str1_len]u8 = "abcd".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: small, not equal, same length" { + const str1_len = 3; + var str1: [str1_len]u8 = "acb".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, equal" { + const content = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content, content.len); + const roc_str2 = RocStr.init(content, content.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, different lengths, unequal" { + const content1 = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content1, content1.len); + const content2 = "012345678901234567890"; + const roc_str2 = RocStr.init(content2, content2.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, different content, unequal" { + const content1 = "012345678901234567890123456789!!"; + const roc_str1 = RocStr.init(content1, content1.len); + const content2 = "012345678901234567890123456789--"; + const roc_str2 = RocStr.init(content2, content2.len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(!roc_str1.eq(roc_str2)); + } + + test "RocStr.eq: large, garbage after end, equal" { + const content = "012345678901234567890123456789"; + const roc_str1 = RocStr.init(content, content.len); + const roc_str2 = RocStr.init(content, content.len); + try expect(roc_str1.bytes != roc_str2.bytes); + + // Insert garbage after the end of each string + roc_str1.bytes.?[30] = '!'; + roc_str1.bytes.?[31] = '!'; + roc_str2.bytes.?[30] = '-'; + roc_str2.bytes.?[31] = '-'; + + defer { + roc_str1.decref(); + roc_str2.decref(); + } + + try expect(roc_str1.eq(roc_str2)); + } +}; + +pub fn init(bytes_ptr: [*]const u8, length: usize) callconv(.C) RocStr { + return @call(.always_inline, RocStr.init, .{ bytes_ptr, length }); +} + +// Str.equal +pub fn strEqual(self: RocStr, other: RocStr) callconv(.C) bool { + return self.eq(other); +} + +// Str.numberOfBytes +pub fn strNumberOfBytes(string: RocStr) callconv(.C) usize { + return string.len(); +} + +// Str.fromInt +pub fn exportFromInt(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(int: T) callconv(.C) RocStr { + return @call(.always_inline, strFromIntHelp, .{ T, int }); + } + }.func; + + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn strFromIntHelp(comptime T: type, int: T) RocStr { + // determine maximum size for this T + const size = comptime blk: { + // the string representation of the minimum i128 value uses at most 40 characters + var buf: [40]u8 = undefined; + var resultMin = std.fmt.bufPrint(&buf, "{}", .{std.math.minInt(T)}) catch unreachable; + var resultMax = std.fmt.bufPrint(&buf, "{}", .{std.math.maxInt(T)}) catch unreachable; + var result = if (resultMin.len > resultMax.len) resultMin.len else resultMax.len; + break :blk result; + }; + + var buf: [size]u8 = undefined; + const result = std.fmt.bufPrint(&buf, "{}", .{int}) catch unreachable; + + return RocStr.init(&buf, result.len); +} + +// Str.fromFloat +pub fn exportFromFloat(comptime T: type, comptime name: []const u8) void { + comptime var f = struct { + fn func(float: T) callconv(.C) RocStr { + return @call(.always_inline, strFromFloatHelp, .{ T, float }); + } + }.func; + + @export(f, .{ .name = name ++ @typeName(T), .linkage = .Strong }); +} + +fn strFromFloatHelp(comptime T: type, float: T) RocStr { + var buf: [400]u8 = undefined; + const result = std.fmt.bufPrint(&buf, "{d}", .{float}) catch unreachable; + + return RocStr.init(&buf, result.len); +} + +// Str.split +pub fn strSplit(string: RocStr, delimiter: RocStr) callconv(.C) RocList { + const segment_count = countSegments(string, delimiter); + const list = RocList.allocate(@alignOf(RocStr), segment_count, @sizeOf(RocStr)); + + if (list.bytes) |bytes| { + const strings = @as([*]RocStr, @ptrCast(@alignCast(bytes))); + strSplitHelp(strings, string, delimiter); + } + + return list; +} + +fn initFromSmallStr(slice_bytes: [*]u8, len: usize, _: usize) RocStr { + return RocStr.init(slice_bytes, len); +} + +// The alloc_ptr must already be shifted to be ready for storing in a seamless slice. +fn initFromBigStr(slice_bytes: [*]u8, len: usize, alloc_ptr: usize) RocStr { + // Here we can make seamless slices instead of copying to a new small str. + return RocStr{ + .bytes = slice_bytes, + .length = len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = alloc_ptr, + }; +} + +fn strSplitHelp(array: [*]RocStr, string: RocStr, delimiter: RocStr) void { + if (delimiter.len() == 0) { + string.incref(1); + array[0] = string; + return; + } + + var it = std.mem.split(u8, string.asSlice(), delimiter.asSlice()); + + var i: usize = 0; + var offset: usize = 0; + + while (it.next()) |zig_slice| { + const roc_slice = substringUnsafe(string, offset, zig_slice.len); + array[i] = roc_slice; + + i += 1; + offset += zig_slice.len + delimiter.len(); + } + + // Correct refcount for all of the splits made. + string.incref(i); // i == array.len() +} + +test "strSplitHelp: empty delimiter" { + // Str.split "abc" "" == ["abc"] + const str_arr = "abc"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = ""; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [1]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [1]RocStr{ + str, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); +} + +test "strSplitHelp: no delimiter" { + // Str.split "abc" "!" == ["abc"] + const str_arr = "abc"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [1]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [1]RocStr{ + str, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); +} + +test "strSplitHelp: empty start" { + const str_arr = "/a"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "/"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 2; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const one = RocStr.init("a", 1); + + var expected = [2]RocStr{ + RocStr.empty(), one, + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: empty end" { + const str_arr = "1---- ---- ---- ---- ----2---- ---- ---- ---- ----"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "---- ---- ---- ---- ----"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const one = RocStr.init("1", 1); + const two = RocStr.init("2", 1); + + var expected = [3]RocStr{ + one, two, RocStr.empty(), + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +test "strSplitHelp: string equals delimiter" { + const str_delimiter_arr = "/"; + const str_delimiter = RocStr.init(str_delimiter_arr, str_delimiter_arr.len); + + const array_len: usize = 2; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str_delimiter, str_delimiter); + + var expected = [2]RocStr{ RocStr.empty(), RocStr.empty() }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str_delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: delimiter on sides" { + const str_arr = "tttghittt"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "ttt"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = [_]RocStr{ + undefined, + undefined, + undefined, + }; + const array_ptr: [*]RocStr = &array; + strSplitHelp(array_ptr, str, delimiter); + + const ghi_arr = "ghi"; + const ghi = RocStr.init(ghi_arr, ghi_arr.len); + + var expected = [3]RocStr{ + RocStr.empty(), ghi, RocStr.empty(), + }; + + defer { + for (array) |rocStr| { + rocStr.decref(); + } + + for (expected) |rocStr| { + rocStr.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +test "strSplitHelp: three pieces" { + // Str.split "a!b!c" "!" == ["a", "b", "c"] + const str_arr = "a!b!c"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + const array_len: usize = 3; + var array: [array_len]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + const a = RocStr.init("a", 1); + const b = RocStr.init("b", 1); + const c = RocStr.init("c", 1); + + var expected_array = [array_len]RocStr{ + a, b, c, + }; + + defer { + for (array) |roc_str| { + roc_str.decref(); + } + + for (expected_array) |roc_str| { + roc_str.decref(); + } + + str.decref(); + delimiter.decref(); + } + + try expectEqual(expected_array.len, array.len); + try expect(array[0].eq(expected_array[0])); + try expect(array[1].eq(expected_array[1])); + try expect(array[2].eq(expected_array[2])); +} + +test "strSplitHelp: overlapping delimiter 1" { + // Str.split "aaa" "aa" == ["", "a"] + const str_arr = "aaa"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "aa"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [2]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [2]RocStr{ + RocStr.empty(), + RocStr.init("a", 1), + }; + + // strings are all small so we ignore freeing the memory + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); +} + +test "strSplitHelp: overlapping delimiter 2" { + // Str.split "aaa" "aa" == ["", "a"] + const str_arr = "aaaa"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "aa"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + var array: [3]RocStr = undefined; + const array_ptr: [*]RocStr = &array; + + strSplitHelp(array_ptr, str, delimiter); + + var expected = [3]RocStr{ + RocStr.empty(), + RocStr.empty(), + RocStr.empty(), + }; + + // strings are all small so we ignore freeing the memory + + try expectEqual(array.len, expected.len); + try expect(array[0].eq(expected[0])); + try expect(array[1].eq(expected[1])); + try expect(array[2].eq(expected[2])); +} + +// This is used for `Str.split : Str, Str -> Array Str +// It is used to count how many segments the input `_str` +// needs to be broken into, so that we can allocate a array +// of that size. It always returns at least 1. +pub fn countSegments(string: RocStr, delimiter: RocStr) callconv(.C) usize { + if (delimiter.isEmpty()) { + return 1; + } + + var it = std.mem.split(u8, string.asSlice(), delimiter.asSlice()); + var count: usize = 0; + + while (it.next()) |_| : (count += 1) {} + + return count; +} + +test "countSegments: long delimiter" { + // Str.split "str" "delimiter" == ["str"] + // 1 segment + const str_arr = "str"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "delimiter"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + try expectEqual(segments_count, 1); +} + +test "countSegments: delimiter at start" { + // Str.split "hello there" "hello" == ["", " there"] + // 2 segments + const str_arr = "hello there"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "hello"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + + try expectEqual(segments_count, 2); +} + +test "countSegments: delimiter interspered" { + // Str.split "a!b!c" "!" == ["a", "b", "c"] + // 3 segments + const str_arr = "a!b!c"; + const str = RocStr.init(str_arr, str_arr.len); + + const delimiter_arr = "!"; + const delimiter = RocStr.init(delimiter_arr, delimiter_arr.len); + + defer { + str.decref(); + delimiter.decref(); + } + + const segments_count = countSegments(str, delimiter); + + try expectEqual(segments_count, 3); +} + +test "countSegments: string equals delimiter" { + // Str.split "/" "/" == ["", ""] + // 2 segments + const str_delimiter_arr = "/"; + const str_delimiter = RocStr.init(str_delimiter_arr, str_delimiter_arr.len); + + defer { + str_delimiter.decref(); + } + + const segments_count = countSegments(str_delimiter, str_delimiter); + + try expectEqual(segments_count, 2); +} + +test "countSegments: overlapping delimiter 1" { + // Str.split "aaa" "aa" == ["", "a"] + const segments_count = countSegments(RocStr.init("aaa", 3), RocStr.init("aa", 2)); + + try expectEqual(segments_count, 2); +} + +test "countSegments: overlapping delimiter 2" { + // Str.split "aaa" "aa" == ["", "a"] + const segments_count = countSegments(RocStr.init("aaaa", 4), RocStr.init("aa", 2)); + + try expectEqual(segments_count, 3); +} + +pub fn countUtf8Bytes(string: RocStr) callconv(.C) u64 { + return @intCast(string.len()); +} + +pub fn isEmpty(string: RocStr) callconv(.C) bool { + return string.isEmpty(); +} + +pub fn getCapacity(string: RocStr) callconv(.C) usize { + return string.getCapacity(); +} + +pub fn substringUnsafeC(string: RocStr, start_u64: u64, length_u64: u64) callconv(.C) RocStr { + const start: usize = @intCast(start_u64); + const length: usize = @intCast(length_u64); + + return substringUnsafe(string, start, length); +} + +fn substringUnsafe(string: RocStr, start: usize, length: usize) RocStr { + if (string.isSmallStr()) { + if (start == 0) { + var output = string; + output.setLen(length); + return output; + } + const slice = string.asSlice()[start .. start + length]; + return RocStr.fromSlice(slice); + } + if (string.bytes) |source_ptr| { + if (start == 0 and string.isUnique()) { + var output = string; + output.setLen(length); + return output; + } else { + // Shifting right by 1 is required to avoid the highest bit of capacity being set. + // If it was set, the slice would get interpreted as a small string. + const str_alloc_ptr = (@intFromPtr(source_ptr) >> 1); + const slice_alloc_ptr = string.capacity_or_alloc_ptr; + const slice_mask = string.seamlessSliceMask(); + const alloc_ptr = (str_alloc_ptr & ~slice_mask) | (slice_alloc_ptr & slice_mask); + return RocStr{ + .bytes = source_ptr + start, + .length = length | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = alloc_ptr, + }; + } + } + return RocStr.empty(); +} + +pub fn getUnsafeC(string: RocStr, index: u64) callconv(.C) u8 { + return string.getUnchecked(@intCast(index)); +} + +test "substringUnsafe: start" { + const str = RocStr.fromSlice("abcdef"); + defer str.decref(); + + const expected = RocStr.fromSlice("abc"); + defer expected.decref(); + + const actual = substringUnsafe(str, 0, 3); + + try expect(RocStr.eq(actual, expected)); +} + +test "substringUnsafe: middle" { + const str = RocStr.fromSlice("abcdef"); + defer str.decref(); + + const expected = RocStr.fromSlice("bcd"); + defer expected.decref(); + + const actual = substringUnsafe(str, 1, 3); + + try expect(RocStr.eq(actual, expected)); +} + +test "substringUnsafe: end" { + const str = RocStr.fromSlice("a string so long it is heap-allocated"); + defer str.decref(); + + const expected = RocStr.fromSlice("heap-allocated"); + defer expected.decref(); + + const actual = substringUnsafe(str, 23, 37 - 23); + + try expect(RocStr.eq(actual, expected)); +} + +// Str.startsWith +pub fn startsWith(string: RocStr, prefix: RocStr) callconv(.C) bool { + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + const prefix_len = prefix.len(); + const prefix_ptr = prefix.asU8ptr(); + + if (prefix_len > bytes_len) { + return false; + } + + // we won't exceed bytes_len due to the previous check + var i: usize = 0; + while (i < prefix_len) { + if (bytes_ptr[i] != prefix_ptr[i]) { + return false; + } + i += 1; + } + return true; +} + +// Str.repeat +pub fn repeatC(string: RocStr, count_u64: u64) callconv(.C) RocStr { + const count: usize = @intCast(count_u64); + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + var ret_string = RocStr.allocate(count * bytes_len); + var ret_string_ptr = ret_string.asU8ptrMut(); + + var i: usize = 0; + while (i < count) : (i += 1) { + @memcpy(ret_string_ptr[0..bytes_len], bytes_ptr[0..bytes_len]); + ret_string_ptr += bytes_len; + } + + return ret_string; +} + +test "startsWith: foo starts with fo" { + const foo = RocStr.fromSlice("foo"); + const fo = RocStr.fromSlice("fo"); + try expect(startsWith(foo, fo)); +} + +test "startsWith: 123456789123456789 starts with 123456789123456789" { + const str = RocStr.fromSlice("123456789123456789"); + defer str.decref(); + try expect(startsWith(str, str)); +} + +test "startsWith: 12345678912345678910 starts with 123456789123456789" { + const str = RocStr.fromSlice("12345678912345678910"); + defer str.decref(); + const prefix = RocStr.fromSlice("123456789123456789"); + defer prefix.decref(); + + try expect(startsWith(str, prefix)); +} + +// Str.endsWith +pub fn endsWith(string: RocStr, suffix: RocStr) callconv(.C) bool { + const bytes_len = string.len(); + const bytes_ptr = string.asU8ptr(); + + const suffix_len = suffix.len(); + const suffix_ptr = suffix.asU8ptr(); + + if (suffix_len > bytes_len) { + return false; + } + + const offset: usize = bytes_len - suffix_len; + var i: usize = 0; + while (i < suffix_len) { + if (bytes_ptr[i + offset] != suffix_ptr[i]) { + return false; + } + i += 1; + } + return true; +} + +test "endsWith: foo ends with oo" { + const foo = RocStr.init("foo", 3); + const oo = RocStr.init("oo", 2); + defer foo.decref(); + defer oo.decref(); + + try expect(endsWith(foo, oo)); +} + +test "endsWith: 123456789123456789 ends with 123456789123456789" { + const str = RocStr.init("123456789123456789", 18); + defer str.decref(); + try expect(endsWith(str, str)); +} + +test "endsWith: 12345678912345678910 ends with 345678912345678910" { + const str = RocStr.init("12345678912345678910", 20); + const suffix = RocStr.init("345678912345678910", 18); + defer str.decref(); + defer suffix.decref(); + + try expect(endsWith(str, suffix)); +} + +test "endsWith: hello world ends with world" { + const str = RocStr.init("hello world", 11); + const suffix = RocStr.init("world", 5); + defer str.decref(); + defer suffix.decref(); + + try expect(endsWith(str, suffix)); +} + +// Str.concat +pub fn strConcatC(arg1: RocStr, arg2: RocStr) callconv(.C) RocStr { + return @call(.always_inline, strConcat, .{ arg1, arg2 }); +} + +fn strConcat(arg1: RocStr, arg2: RocStr) RocStr { + // NOTE: we don't special-case the first argument being empty. That is because it is owned and + // may have sufficient capacity to store the rest of the list. + if (arg2.isEmpty()) { + // the first argument is owned, so we can return it without cloning + return arg1; + } else { + const combined_length = arg1.len() + arg2.len(); + + var result = arg1.reallocate(combined_length); + @memcpy(result.asU8ptrMut()[arg1.len()..combined_length], arg2.asU8ptr()[0..arg2.len()]); + + return result; + } +} + +test "RocStr.concat: small concat small" { + const str1_len = 3; + var str1: [str1_len]u8 = "foo".*; + const str1_ptr: [*]u8 = &str1; + var roc_str1 = RocStr.init(str1_ptr, str1_len); + + const str2_len = 3; + var str2: [str2_len]u8 = "abc".*; + const str2_ptr: [*]u8 = &str2; + var roc_str2 = RocStr.init(str2_ptr, str2_len); + + const str3_len = 6; + var str3: [str3_len]u8 = "fooabc".*; + const str3_ptr: [*]u8 = &str3; + var roc_str3 = RocStr.init(str3_ptr, str3_len); + + defer { + roc_str1.decref(); + roc_str2.decref(); + roc_str3.decref(); + } + + const result = strConcat(roc_str1, roc_str2); + + defer result.decref(); + + try expect(roc_str3.eq(result)); +} + +pub const RocListStr = extern struct { + list_elements: ?[*]RocStr, + list_length: usize, + list_capacity_or_alloc_ptr: usize, +}; + +// Str.joinWith +pub fn strJoinWithC(list: RocList, separator: RocStr) callconv(.C) RocStr { + const roc_list_str = RocListStr{ + .list_elements = @as(?[*]RocStr, @ptrCast(@alignCast(list.bytes))), + .list_length = list.length, + .list_capacity_or_alloc_ptr = list.capacity_or_alloc_ptr, + }; + + return @call(.always_inline, strJoinWith, .{ roc_list_str, separator }); +} + +fn strJoinWith(list: RocListStr, separator: RocStr) RocStr { + const len = list.list_length; + + if (len == 0) { + return RocStr.empty(); + } else { + const ptr = @as([*]RocStr, @ptrCast(list.list_elements)); + const slice: []RocStr = ptr[0..len]; + + // determine the size of the result + var total_size: usize = 0; + for (slice) |substr| { + total_size += substr.len(); + } + + // include size of the separator + total_size += separator.len() * (len - 1); + + var result = RocStr.allocate(total_size); + var result_ptr = result.asU8ptrMut(); + + var offset: usize = 0; + for (slice[0 .. len - 1]) |substr| { + substr.memcpy(result_ptr + offset); + offset += substr.len(); + + separator.memcpy(result_ptr + offset); + offset += separator.len(); + } + + const substr = slice[len - 1]; + substr.memcpy(result_ptr + offset); + + return result; + } +} + +test "RocStr.joinWith: result is big" { + const sep_len = 2; + var sep: [sep_len]u8 = ", ".*; + const sep_ptr: [*]u8 = &sep; + var roc_sep = RocStr.init(sep_ptr, sep_len); + + const elem_len = 13; + var elem: [elem_len]u8 = "foobarbazspam".*; + const elem_ptr: [*]u8 = &elem; + var roc_elem = RocStr.init(elem_ptr, elem_len); + + const result_len = 43; + var xresult: [result_len]u8 = "foobarbazspam, foobarbazspam, foobarbazspam".*; + const result_ptr: [*]u8 = &xresult; + var roc_result = RocStr.init(result_ptr, result_len); + + var elements: [3]RocStr = .{ roc_elem, roc_elem, roc_elem }; + const list = RocListStr{ + .list_length = 3, + .list_capacity_or_alloc_ptr = 3, + .list_elements = @as([*]RocStr, @ptrCast(&elements)), + }; + + defer { + roc_sep.decref(); + roc_elem.decref(); + roc_result.decref(); + } + + const result = strJoinWith(list, roc_sep); + + defer result.decref(); + + try expect(roc_result.eq(result)); +} + +// Str.toUtf8 +pub fn strToUtf8C(arg: RocStr) callconv(.C) RocList { + return strToBytes(arg); +} + +inline fn strToBytes(arg: RocStr) RocList { + const length = arg.len(); + if (length == 0) { + return RocList.empty(); + } else if (arg.isSmallStr()) { + const ptr = utils.allocateWithRefcount(length, RocStr.alignment); + + @memcpy(ptr[0..length], arg.asU8ptr()[0..length]); + + return RocList{ .length = length, .bytes = ptr, .capacity_or_alloc_ptr = length }; + } else { + const is_seamless_slice = arg.length & SEAMLESS_SLICE_BIT; + return RocList{ .length = length, .bytes = arg.bytes, .capacity_or_alloc_ptr = arg.capacity_or_alloc_ptr | is_seamless_slice }; + } +} + +const FromUtf8Result = extern struct { + byte_index: u64, + string: RocStr, + is_ok: bool, + problem_code: Utf8ByteProblem, +}; + +pub fn fromUtf8C( + list: RocList, + update_mode: UpdateMode, +) callconv(.C) FromUtf8Result { + return fromUtf8(list, update_mode); +} + +pub fn fromUtf8( + list: RocList, + update_mode: UpdateMode, +) FromUtf8Result { + if (list.len() == 0) { + list.decref(1); // Alignment 1 for List U8 + return FromUtf8Result{ + .is_ok = true, + .string = RocStr.empty(), + .byte_index = 0, + .problem_code = Utf8ByteProblem.InvalidStartByte, + }; + } + const bytes = @as([*]const u8, @ptrCast(list.bytes))[0..list.len()]; + + if (isValidUnicode(bytes)) { + // Make a seamless slice of the input. + const string = RocStr.fromSubListUnsafe(list, 0, list.len(), update_mode); + return FromUtf8Result{ + .is_ok = true, + .string = string, + .byte_index = 0, + .problem_code = Utf8ByteProblem.InvalidStartByte, + }; + } else { + const temp = errorToProblem(bytes); + + list.decref(1); // Alignment 1 for List U8 + + return FromUtf8Result{ + .is_ok = false, + .string = RocStr.empty(), + .byte_index = @intCast(temp.index), + .problem_code = temp.problem, + }; + } +} + +fn errorToProblem(bytes: []const u8) struct { index: usize, problem: Utf8ByteProblem } { + const len = bytes.len; + var index: usize = 0; + + while (index < len) { + const nextNumBytes = numberOfNextCodepointBytes(bytes, index) catch |err| { + switch (err) { + error.UnexpectedEof => { + return .{ .index = index, .problem = Utf8ByteProblem.UnexpectedEndOfSequence }; + }, + error.Utf8InvalidStartByte => return .{ .index = index, .problem = Utf8ByteProblem.InvalidStartByte }, + error.Utf8ExpectedContinuation => return .{ .index = index, .problem = Utf8ByteProblem.ExpectedContinuation }, + error.Utf8OverlongEncoding => return .{ .index = index, .problem = Utf8ByteProblem.OverlongEncoding }, + error.Utf8EncodesSurrogateHalf => return .{ .index = index, .problem = Utf8ByteProblem.EncodesSurrogateHalf }, + error.Utf8CodepointTooLarge => return .{ .index = index, .problem = Utf8ByteProblem.CodepointTooLarge }, + } + }; + index += nextNumBytes; + } + + unreachable; +} + +pub fn isValidUnicode(buf: []const u8) bool { + const size = @sizeOf(u64); + // TODO: we should test changing the step on other platforms. + // The general tradeoff is making extremely large strings potentially much faster + // at the cost of small strings being slightly slower. + const step = size; + var i: usize = 0; + while (i + step < buf.len) { + var bytes: u64 = undefined; + @memcpy(@as([*]u8, @ptrCast(&bytes))[0..size], buf[i..(i + size)]); + const unicode_bytes = bytes & 0x8080_8080_8080_8080; + if (unicode_bytes == 0) { + i += step; + continue; + } + + while (buf[i] < 0b1000_0000) : (i += 1) {} + + while (buf[i] >= 0b1000_0000) { + // This forces prefetching, otherwise the loop can run at about half speed. + if (i + 4 >= buf.len) break; + var small_buf: [4]u8 = undefined; + @memcpy(small_buf[0..4], buf[i..(i + 4)]); + // TODO: Should we always inline these function calls below? + if (std.unicode.utf8ByteSequenceLength(small_buf[0])) |cp_len| { + if (std.meta.isError(std.unicode.utf8Decode(small_buf[0..cp_len]))) { + return false; + } + i += cp_len; + } else |_| { + return false; + } + } + } + + if (i == buf.len) return true; + while (buf[i] < 0b1000_0000) { + i += 1; + if (i == buf.len) return true; + } + + return @call(.always_inline, unicode.utf8ValidateSlice, .{buf[i..]}); +} + +const Utf8DecodeError = error{ + UnexpectedEof, + Utf8InvalidStartByte, + Utf8ExpectedContinuation, + Utf8OverlongEncoding, + Utf8EncodesSurrogateHalf, + Utf8CodepointTooLarge, +}; + +// Essentially unicode.utf8ValidateSlice -> https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L156 +// but only for the next codepoint from the index. Then we return the number of bytes of that codepoint. +// TODO: we only ever use the values 0-4, so can we use smaller int than `usize`? +pub fn numberOfNextCodepointBytes(bytes: []const u8, index: usize) Utf8DecodeError!usize { + const codepoint_len = try unicode.utf8ByteSequenceLength(bytes[index]); + const codepoint_end_index = index + codepoint_len; + if (codepoint_end_index > bytes.len) { + return error.UnexpectedEof; + } + _ = try unicode.utf8Decode(bytes[index..codepoint_end_index]); + return codepoint_end_index - index; +} + +// Return types for validateUtf8Bytes +// Values must be in alphabetical order. That is, lowest values are the first alphabetically. +pub const Utf8ByteProblem = enum(u8) { + CodepointTooLarge = 0, + EncodesSurrogateHalf = 1, + ExpectedContinuation = 2, + InvalidStartByte = 3, + OverlongEncoding = 4, + UnexpectedEndOfSequence = 5, +}; + +fn validateUtf8Bytes(bytes: [*]u8, length: usize) FromUtf8Result { + return fromUtf8(RocList{ .bytes = bytes, .length = length, .capacity_or_alloc_ptr = length }, .Immutable); +} + +fn validateUtf8BytesX(str: RocList) FromUtf8Result { + return fromUtf8(str, .Immutable); +} + +fn expectOk(result: FromUtf8Result) !void { + try expectEqual(result.is_ok, true); +} + +fn sliceHelp(bytes: [*]const u8, length: usize) RocList { + var list = RocList.allocate(RocStr.alignment, length, @sizeOf(u8)); + var list_bytes = list.bytes orelse unreachable; + @memcpy(list_bytes[0..length], bytes[0..length]); + list.length = length; + + return list; +} + +fn toErrUtf8ByteResponse(index: usize, problem: Utf8ByteProblem) FromUtf8Result { + return FromUtf8Result{ .is_ok = false, .string = RocStr.empty(), .byte_index = @as(u64, @intCast(index)), .problem_code = problem }; +} + +// NOTE on memory: the validate function consumes a RC token of the input. Since +// we freshly created it (in `sliceHelp`), it has only one RC token, and input list will be deallocated. +// +// If we tested with big strings, we'd have to deallocate the output string, but never the input list + +test "validateUtf8Bytes: ascii" { + const raw = "abc"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode œ" { + const raw = "œ"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode ∆" { + const raw = "∆"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: emoji" { + const raw = "💖"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +test "validateUtf8Bytes: unicode ∆ in middle of array" { + const raw = "œb∆c¬"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + const str_result = validateUtf8BytesX(list); + defer str_result.string.decref(); + try expectOk(str_result); +} + +fn expectErr(list: RocList, index: usize, err: Utf8DecodeError, problem: Utf8ByteProblem) !void { + const str_ptr = @as([*]u8, @ptrCast(list.bytes)); + const len = list.length; + + try expectError(err, numberOfNextCodepointBytes(str_ptr[0..len], index)); + try expectEqual(toErrUtf8ByteResponse(index, problem), validateUtf8Bytes(str_ptr, len)); +} + +test "validateUtf8Bytes: invalid start byte" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "ab\x80c"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 2, error.Utf8InvalidStartByte, Utf8ByteProblem.InvalidStartByte); +} + +test "validateUtf8Bytes: unexpected eof for 2 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "abc\xc2"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 2 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L426 + const raw = "abc\xc2\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: unexpected eof for 3 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 + const raw = "abc\xe0\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 3 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L430 + const raw = "abc\xe0\xa0\xc0"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: unexpected eof for 4 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 + const raw = "abc\xf0\x90\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.UnexpectedEof, Utf8ByteProblem.UnexpectedEndOfSequence); +} + +test "validateUtf8Bytes: expected continuation for 4 byte sequence" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L437 + const raw = "abc\xf0\x90\x80\x00"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8ExpectedContinuation, Utf8ByteProblem.ExpectedContinuation); +} + +test "validateUtf8Bytes: overlong" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L451 + const raw = "abc\xf0\x80\x80\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8OverlongEncoding, Utf8ByteProblem.OverlongEncoding); +} + +test "validateUtf8Bytes: codepoint out too large" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L465 + const raw = "abc\xf4\x90\x80\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8CodepointTooLarge, Utf8ByteProblem.CodepointTooLarge); +} + +test "validateUtf8Bytes: surrogate halves" { + // https://github.com/ziglang/zig/blob/0.7.x/lib/std/unicode.zig#L468 + const raw = "abc\xed\xa0\x80"; + const ptr: [*]const u8 = @as([*]const u8, @ptrCast(raw)); + const list = sliceHelp(ptr, raw.len); + + try expectErr(list, 3, error.Utf8EncodesSurrogateHalf, Utf8ByteProblem.EncodesSurrogateHalf); +} + +fn isWhitespace(codepoint: u21) bool { + // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt + return switch (codepoint) { + 0x0009...0x000D => true, // control characters + 0x0020 => true, // space + 0x0085 => true, // control character + 0x00A0 => true, // no-break space + 0x1680 => true, // ogham space + 0x2000...0x200A => true, // en quad..hair space + 0x200E...0x200F => true, // left-to-right & right-to-left marks + 0x2028 => true, // line separator + 0x2029 => true, // paragraph separator + 0x202F => true, // narrow no-break space + 0x205F => true, // medium mathematical space + 0x3000 => true, // ideographic space + + else => false, + }; +} + +test "isWhitespace" { + try expect(isWhitespace(' ')); + try expect(isWhitespace('\u{00A0}')); + try expect(!isWhitespace('x')); +} + +pub fn strTrim(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const leading_bytes = countLeadingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == leading_bytes) { + string.decref(); + return RocStr.empty(); + } + + const trailing_bytes = countTrailingWhitespaceBytes(string); + const new_len = original_len - leading_bytes - trailing_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr() + leading_bytes, new_len); + } else if (leading_bytes == 0 and string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.length = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .bytes = bytes_ptr + leading_bytes, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = string.capacity_or_alloc_ptr, + }; + } else { + // Not unique or removing leading bytes, just make a slice. + return RocStr{ + .bytes = bytes_ptr + leading_bytes, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +pub fn strTrimStart(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const leading_bytes = countLeadingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == leading_bytes) { + string.decref(); + return RocStr.empty(); + } + + const new_len = original_len - leading_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr() + leading_bytes, new_len); + } else if (leading_bytes == 0 and string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.length = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .bytes = bytes_ptr + leading_bytes, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = string.capacity_or_alloc_ptr, + }; + } else { + // Not unique or removing leading bytes, just make a slice. + return RocStr{ + .bytes = bytes_ptr + leading_bytes, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +pub fn strTrimEnd(input_string: RocStr) callconv(.C) RocStr { + var string = input_string; + + if (string.isEmpty()) { + string.decref(); + return RocStr.empty(); + } + + const bytes_ptr = string.asU8ptrMut(); + + const trailing_bytes = countTrailingWhitespaceBytes(string); + const original_len = string.len(); + + if (original_len == trailing_bytes) { + string.decref(); + return RocStr.empty(); + } + + const new_len = original_len - trailing_bytes; + + if (string.isSmallStr()) { + // Just create another small string of the correct bytes. + // No need to decref because it is a small string. + return RocStr.init(string.asU8ptr(), new_len); + } else if (string.isUnique()) { + // Big and unique with no leading bytes to remove. + // Just take ownership and shrink the length. + var new_string = string; + new_string.length = new_len; + + return new_string; + } else if (string.isSeamlessSlice()) { + // Already a seamless slice, just update the range. + return RocStr{ + .bytes = bytes_ptr, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = string.capacity_or_alloc_ptr, + }; + } else { + // Not unique, just make a slice. + return RocStr{ + .bytes = bytes_ptr, + .length = new_len | SEAMLESS_SLICE_BIT, + .capacity_or_alloc_ptr = @intFromPtr(bytes_ptr) >> 1, + }; + } +} + +fn countLeadingWhitespaceBytes(string: RocStr) usize { + var byte_count: usize = 0; + + var bytes = string.asU8ptr()[0..string.len()]; + var iter = unicode.Utf8View.initUnchecked(bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + if (isWhitespace(codepoint)) { + byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; + } else { + break; + } + } + + return byte_count; +} + +fn countTrailingWhitespaceBytes(string: RocStr) usize { + var byte_count: usize = 0; + + var bytes = string.asU8ptr()[0..string.len()]; + var iter = ReverseUtf8View.initUnchecked(bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + if (isWhitespace(codepoint)) { + byte_count += unicode.utf8CodepointSequenceLength(codepoint) catch break; + } else { + break; + } + } + + return byte_count; +} + +/// A backwards version of Utf8View from std.unicode +const ReverseUtf8View = struct { + bytes: []const u8, + + pub fn initUnchecked(s: []const u8) ReverseUtf8View { + return ReverseUtf8View{ .bytes = s }; + } + + pub fn iterator(s: ReverseUtf8View) ReverseUtf8Iterator { + return ReverseUtf8Iterator{ + .bytes = s.bytes, + .i = if (s.bytes.len > 0) s.bytes.len - 1 else null, + }; + } +}; + +/// A backwards version of Utf8Iterator from std.unicode +const ReverseUtf8Iterator = struct { + bytes: []const u8, + // NOTE null signifies complete/empty + i: ?usize, + + pub fn nextCodepointSlice(it: *ReverseUtf8Iterator) ?[]const u8 { + if (it.i) |index| { + var i = index; + + // NOTE this relies on the string being valid utf8 to not run off the end + while (!utf8BeginByte(it.bytes[i])) { + i -= 1; + } + + const cp_len = unicode.utf8ByteSequenceLength(it.bytes[i]) catch unreachable; + const slice = it.bytes[i .. i + cp_len]; + + it.i = if (i == 0) null else i - 1; + + return slice; + } else { + return null; + } + } + + pub fn nextCodepoint(it: *ReverseUtf8Iterator) ?u21 { + const slice = it.nextCodepointSlice() orelse return null; + + return switch (slice.len) { + 1 => @as(u21, slice[0]), + 2 => unicode.utf8Decode2(slice) catch unreachable, + 3 => unicode.utf8Decode3(slice) catch unreachable, + 4 => unicode.utf8Decode4(slice) catch unreachable, + else => unreachable, + }; + } +}; + +fn utf8BeginByte(byte: u8) bool { + return switch (byte) { + 0b1000_0000...0b1011_1111 => false, + else => true, + }; +} + +test "strTrim: empty" { + const trimmedEmpty = strTrim(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrim: null byte" { + const bytes = [_]u8{0}; + const original = RocStr.init(&bytes, 1); + + try expectEqual(@as(usize, 1), original.len()); + try expectEqual(@as(usize, SMALL_STR_MAX_LENGTH), original.getCapacity()); + + const original_with_capacity = reserve(original, 40); + defer original_with_capacity.decref(); + + try expectEqual(@as(usize, 1), original_with_capacity.len()); + try expectEqual(@as(usize, 64), original_with_capacity.getCapacity()); + + const trimmed = strTrim(original.clone()); + defer trimmed.decref(); + + try expect(original.eq(trimmed)); +} + +test "strTrim: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrim: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello even more giant world"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); +} + +test "strTrim: large to small sized slice" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + try expect(original.isUnique()); + const trimmed = strTrim(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrim: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = "hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrim(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "strTrimStart: empty" { + const trimmedEmpty = strTrimStart(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrimStart: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrimStart: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello even more giant world "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(expected)); +} + +test "strTrimStart: large to small" { + // `original` will be consumed by the concat; do not free explicitly + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = "hello "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimStart(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrimStart: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = "hello "; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimStart(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "strTrimEnd: empty" { + const trimmedEmpty = strTrimEnd(RocStr.empty()); + try expect(trimmedEmpty.eq(RocStr.empty())); +} + +test "strTrimEnd: blank" { + const original_bytes = " "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(RocStr.empty())); +} + +test "strTrimEnd: large to large" { + const original_bytes = " hello even more giant world "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(!original.isSmallStr()); + + const expected_bytes = " hello even more giant world"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(!expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(expected)); +} + +test "strTrimEnd: large to small" { + // `original` will be consumed by the concat; do not free explicitly + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + + try expect(!original.isSmallStr()); + + const expected_bytes = " hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + defer trimmed.decref(); + + try expect(trimmed.eq(expected)); + try expect(!trimmed.isSmallStr()); +} + +test "strTrimEnd: small to small" { + const original_bytes = " hello "; + const original = RocStr.init(original_bytes, original_bytes.len); + defer original.decref(); + + try expect(original.isSmallStr()); + + const expected_bytes = " hello"; + const expected = RocStr.init(expected_bytes, expected_bytes.len); + defer expected.decref(); + + try expect(expected.isSmallStr()); + + const trimmed = strTrimEnd(original); + + try expect(trimmed.eq(expected)); + try expect(trimmed.isSmallStr()); +} + +test "ReverseUtf8View: hello world" { + const original_bytes = "hello world"; + const expected_bytes = "dlrow olleh"; + + var i: usize = 0; + var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); + while (iter.nextCodepoint()) |codepoint| { + try expect(expected_bytes[i] == codepoint); + i += 1; + } +} + +test "ReverseUtf8View: empty" { + const original_bytes = ""; + + var iter = ReverseUtf8View.initUnchecked(original_bytes).iterator(); + while (iter.nextCodepoint()) |_| { + try expect(false); + } +} + +test "capacity: small string" { + const data_bytes = "foobar"; + var data = RocStr.init(data_bytes, data_bytes.len); + defer data.decref(); + + try expectEqual(data.getCapacity(), SMALL_STR_MAX_LENGTH); +} + +test "capacity: big string" { + const data_bytes = "a string so large that it must be heap-allocated"; + var data = RocStr.init(data_bytes, data_bytes.len); + defer data.decref(); + + try expect(data.getCapacity() >= data_bytes.len); +} + +pub fn reserveC(string: RocStr, spare_u64: u64) callconv(.C) RocStr { + return reserve(string, @intCast(spare_u64)); +} + +fn reserve(string: RocStr, spare: usize) RocStr { + const old_length = string.len(); + + if (string.getCapacity() >= old_length + spare) { + return string; + } else { + var output = string.reallocate(old_length + spare); + output.setLen(old_length); + return output; + } +} + +pub fn withCapacityC(capacity: u64) callconv(.C) RocStr { + var str = RocStr.allocate(@intCast(capacity)); + str.setLen(0); + return str; +} + +pub fn strCloneTo( + string: RocStr, + ptr: [*]u8, + offset: usize, + extra_offset: usize, +) callconv(.C) usize { + const WIDTH: usize = @sizeOf(RocStr); + if (string.isSmallStr()) { + const array: [@sizeOf(RocStr)]u8 = @as([@sizeOf(RocStr)]u8, @bitCast(string)); + + var i: usize = 0; + while (i < WIDTH) : (i += 1) { + ptr[offset + i] = array[i]; + } + + return extra_offset; + } else { + const slice = string.asSlice(); + + var relative = string; + relative.bytes = @as(?[*]u8, @ptrFromInt(extra_offset)); // i.e. just after the string struct + + // write the string struct + const array = relative.asArray(); + @memcpy(ptr[offset..(offset + WIDTH)], array[0..WIDTH]); + + // write the string bytes just after the struct + @memcpy(ptr[extra_offset..(extra_offset + slice.len)], slice); + + return extra_offset + slice.len; + } +} + +pub fn strAllocationPtr( + string: RocStr, +) callconv(.C) ?[*]u8 { + return string.getAllocationPtr(); +} + +pub fn strReleaseExcessCapacity( + string: RocStr, +) callconv(.C) RocStr { + const old_length = string.len(); + // We use the direct list.capacity_or_alloc_ptr to make sure both that there is no extra capacity and that it isn't a seamless slice. + if (string.isSmallStr()) { + // SmallStr has no excess capacity. + return string; + } else if (string.isUnique() and !string.isSeamlessSlice() and string.getCapacity() == old_length) { + return string; + } else if (old_length == 0) { + string.decref(); + return RocStr.empty(); + } else { + var output = RocStr.allocateExact(old_length); + const source_ptr = string.asU8ptr(); + const dest_ptr = output.asU8ptrMut(); + + @memcpy(dest_ptr[0..old_length], source_ptr[0..old_length]); + string.decref(); + + return output; + } +} diff --git a/examples/platform-switching/web-assembly/host/glue/utils.zig b/examples/platform-switching/web-assembly/host/glue/utils.zig new file mode 100644 index 00000000000..2dd3f1fd2ac --- /dev/null +++ b/examples/platform-switching/web-assembly/host/glue/utils.zig @@ -0,0 +1,531 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const Monotonic = std.builtin.AtomicOrder.Monotonic; + +const DEBUG_INCDEC = false; +const DEBUG_TESTING_ALLOC = false; +const DEBUG_ALLOC = false; + +pub fn WithOverflow(comptime T: type) type { + return extern struct { value: T, has_overflowed: bool }; +} + +// If allocation fails, this must cxa_throw - it must not return a null pointer! +extern fn roc_alloc(size: usize, alignment: u32) callconv(.C) ?*anyopaque; + +// This should never be passed a null pointer. +// If allocation fails, this must cxa_throw - it must not return a null pointer! +extern fn roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, alignment: u32) callconv(.C) ?*anyopaque; + +// This should never be passed a null pointer. +extern fn roc_dealloc(c_ptr: *anyopaque, alignment: u32) callconv(.C) void; + +extern fn roc_dbg(loc: *anyopaque, message: *anyopaque, src: *anyopaque) callconv(.C) void; + +// Since roc_dbg is never used by the builtins, we need at export a function that uses it to stop DCE. +pub fn test_dbg(loc: *anyopaque, src: *anyopaque, message: *anyopaque) callconv(.C) void { + roc_dbg(loc, message, src); +} + +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 testing_roc_getppid() callconv(.C) c_int { + return getppid(); +} + +fn roc_getppid_windows_stub() callconv(.C) c_int { + return 0; +} + +fn testing_roc_shm_open(name: *const i8, oflag: c_int, mode: c_uint) callconv(.C) c_int { + return shm_open(name, oflag, mode); +} +fn testing_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); +} + +fn testing_roc_dbg(loc: *anyopaque, message: *anyopaque, src: *anyopaque) callconv(.C) void { + _ = message; + _ = src; + _ = loc; +} + +comptime { + // During tests, use the testing allocators to satisfy these functions. + if (builtin.is_test) { + @export(testing_roc_alloc, .{ .name = "roc_alloc", .linkage = .Strong }); + @export(testing_roc_realloc, .{ .name = "roc_realloc", .linkage = .Strong }); + @export(testing_roc_dealloc, .{ .name = "roc_dealloc", .linkage = .Strong }); + @export(testing_roc_panic, .{ .name = "roc_panic", .linkage = .Strong }); + @export(testing_roc_dbg, .{ .name = "roc_dbg", .linkage = .Strong }); + + if (builtin.os.tag == .macos or builtin.os.tag == .linux) { + @export(testing_roc_getppid, .{ .name = "roc_getppid", .linkage = .Strong }); + @export(testing_roc_mmap, .{ .name = "roc_mmap", .linkage = .Strong }); + @export(testing_roc_shm_open, .{ .name = "roc_shm_open", .linkage = .Strong }); + } + + if (builtin.os.tag == .windows) { + @export(roc_getppid_windows_stub, .{ .name = "roc_getppid", .linkage = .Strong }); + } + } +} + +fn testing_roc_alloc(size: usize, _: u32) callconv(.C) ?*anyopaque { + // We store an extra usize which is the size of the full allocation. + const full_size = size + @sizeOf(usize); + var raw_ptr = (std.testing.allocator.alloc(u8, full_size) catch unreachable).ptr; + @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0] = full_size; + raw_ptr += @sizeOf(usize); + const ptr = @as(?*anyopaque, @ptrCast(raw_ptr)); + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("+ alloc {*}: {} bytes\n", .{ ptr, size }); + } + + return ptr; +} + +fn testing_roc_realloc(c_ptr: *anyopaque, new_size: usize, old_size: usize, _: u32) callconv(.C) ?*anyopaque { + const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize); + const slice = raw_ptr[0..(old_size + @sizeOf(usize))]; + + const new_full_size = new_size + @sizeOf(usize); + var new_raw_ptr = (std.testing.allocator.realloc(slice, new_full_size) catch unreachable).ptr; + @as([*]usize, @alignCast(@ptrCast(new_raw_ptr)))[0] = new_full_size; + new_raw_ptr += @sizeOf(usize); + const new_ptr = @as(?*anyopaque, @ptrCast(new_raw_ptr)); + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("- realloc {*}\n", .{new_ptr}); + } + + return new_ptr; +} + +fn testing_roc_dealloc(c_ptr: *anyopaque, _: u32) callconv(.C) void { + const raw_ptr = @as([*]u8, @ptrCast(c_ptr)) - @sizeOf(usize); + const full_size = @as([*]usize, @alignCast(@ptrCast(raw_ptr)))[0]; + const slice = raw_ptr[0..full_size]; + + if (DEBUG_TESTING_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("💀 dealloc {*}\n", .{slice.ptr}); + } + + std.testing.allocator.free(slice); +} + +fn testing_roc_panic(c_ptr: *anyopaque, tag_id: u32) callconv(.C) void { + _ = c_ptr; + _ = tag_id; + + @panic("Roc panicked"); +} + +pub fn alloc(size: usize, alignment: u32) ?[*]u8 { + return @as(?[*]u8, @ptrCast(roc_alloc(size, alignment))); +} + +pub fn realloc(c_ptr: [*]u8, new_size: usize, old_size: usize, alignment: u32) [*]u8 { + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("- realloc {*}\n", .{c_ptr}); + } + return @as([*]u8, @ptrCast(roc_realloc(c_ptr, new_size, old_size, alignment))); +} + +pub fn dealloc(c_ptr: [*]u8, alignment: u32) void { + return roc_dealloc(c_ptr, alignment); +} + +// indirection because otherwise zig creates an alias to the panic function which our LLVM code +// does not know how to deal with +pub fn test_panic(c_ptr: *anyopaque, crash_tag: u32) callconv(.C) void { + _ = c_ptr; + _ = crash_tag; + + // const cstr = @ptrCast([*:0]u8, c_ptr); + // + // const stderr = std.io.getStdErr().writer(); + // stderr.print("Roc panicked: {s}!\n", .{cstr}) catch unreachable; + // + // std.c.exit(1); +} + +pub const Inc = fn (?[*]u8) callconv(.C) void; +pub const IncN = fn (?[*]u8, u64) callconv(.C) void; +pub const Dec = fn (?[*]u8) callconv(.C) void; + +const REFCOUNT_MAX_ISIZE: isize = 0; +pub const REFCOUNT_ONE_ISIZE: isize = std.math.minInt(isize); +pub const REFCOUNT_ONE: usize = @as(usize, @bitCast(REFCOUNT_ONE_ISIZE)); + +pub const IntWidth = enum(u8) { + U8 = 0, + U16 = 1, + U32 = 2, + U64 = 3, + U128 = 4, + I8 = 5, + I16 = 6, + I32 = 7, + I64 = 8, + I128 = 9, +}; + +const Refcount = enum { + none, + normal, + atomic, +}; + +const RC_TYPE = Refcount.normal; + +pub fn increfRcPtrC(ptr_to_refcount: *isize, amount: isize) callconv(.C) void { + if (RC_TYPE == Refcount.none) return; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| increment {*}: ", .{ptr_to_refcount}); + } + + // Ensure that the refcount is not whole program lifetime. + if (ptr_to_refcount.* != REFCOUNT_MAX_ISIZE) { + // Note: we assume that a refcount will never overflow. + // As such, we do not need to cap incrementing. + switch (RC_TYPE) { + Refcount.normal => { + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + const old = @as(usize, @bitCast(ptr_to_refcount.*)); + const new = old + @as(usize, @intCast(amount)); + + const oldH = old - REFCOUNT_ONE + 1; + const newH = new - REFCOUNT_ONE + 1; + + std.debug.print("{} + {} = {}!\n", .{ oldH, amount, newH }); + } + + ptr_to_refcount.* += amount; + }, + Refcount.atomic => { + _ = @atomicRmw(isize, ptr_to_refcount, std.builtin.AtomicRmwOp.Add, amount, Monotonic); + }, + Refcount.none => unreachable, + } + } +} + +pub fn decrefRcPtrC( + bytes_or_null: ?[*]isize, + alignment: u32, +) callconv(.C) void { + // IMPORTANT: bytes_or_null is this case is expected to be a pointer to the refcount + // (NOT the start of the data, or the start of the allocation) + + // this is of course unsafe, but we trust what we get from the llvm side + var bytes = @as([*]isize, @ptrCast(bytes_or_null)); + + return @call(.always_inline, decref_ptr_to_refcount, .{ bytes, alignment }); +} + +pub fn decrefCheckNullC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + if (bytes_or_null) |bytes| { + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + return @call(.always_inline, decref_ptr_to_refcount, .{ isizes - 1, alignment }); + } +} + +pub fn decrefDataPtrC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const data_ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const unmasked_ptr = data_ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(unmasked_ptr)); + const rc_ptr = isizes - 1; + + return decrefRcPtrC(rc_ptr, alignment); +} + +pub fn increfDataPtrC( + bytes_or_null: ?[*]u8, + inc_amount: isize, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: *isize = @as(*isize, @ptrFromInt(masked_ptr - @sizeOf(usize))); + + return increfRcPtrC(isizes, inc_amount); +} + +pub fn freeDataPtrC( + bytes_or_null: ?[*]u8, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); + + // we always store the refcount right before the data + return freeRcPtrC(isizes - 1, alignment); +} + +pub fn freeRcPtrC( + bytes_or_null: ?[*]isize, + alignment: u32, +) callconv(.C) void { + var bytes = bytes_or_null orelse return; + return free_ptr_to_refcount(bytes, alignment); +} + +pub fn decref( + bytes_or_null: ?[*]u8, + data_bytes: usize, + alignment: u32, +) void { + if (data_bytes == 0) { + return; + } + + var bytes = bytes_or_null orelse return; + + const isizes: [*]isize = @as([*]isize, @ptrCast(@alignCast(bytes))); + + decref_ptr_to_refcount(isizes - 1, alignment); +} + +inline fn free_ptr_to_refcount( + refcount_ptr: [*]isize, + alignment: u32, +) void { + if (RC_TYPE == Refcount.none) return; + const extra_bytes = @max(alignment, @sizeOf(usize)); + const allocation_ptr = @as([*]u8, @ptrCast(refcount_ptr)) - (extra_bytes - @sizeOf(usize)); + + // NOTE: we don't even check whether the refcount is "infinity" here! + dealloc(allocation_ptr, alignment); + + if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("💀 freed {*}\n", .{allocation_ptr}); + } +} + +inline fn decref_ptr_to_refcount( + refcount_ptr: [*]isize, + alignment: u32, +) void { + if (RC_TYPE == Refcount.none) return; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| decrement {*}: ", .{refcount_ptr}); + } + + // Ensure that the refcount is not whole program lifetime. + const refcount: isize = refcount_ptr[0]; + if (refcount != REFCOUNT_MAX_ISIZE) { + switch (RC_TYPE) { + Refcount.normal => { + const old = @as(usize, @bitCast(refcount)); + refcount_ptr[0] = refcount -% 1; + const new = @as(usize, @bitCast(refcount -% 1)); + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + const oldH = old - REFCOUNT_ONE + 1; + const newH = new - REFCOUNT_ONE + 1; + + std.debug.print("{} - 1 = {}!\n", .{ oldH, newH }); + } + + if (refcount == REFCOUNT_ONE_ISIZE) { + free_ptr_to_refcount(refcount_ptr, alignment); + } + }, + Refcount.atomic => { + var last = @atomicRmw(isize, &refcount_ptr[0], std.builtin.AtomicRmwOp.Sub, 1, Monotonic); + if (last == REFCOUNT_ONE_ISIZE) { + free_ptr_to_refcount(refcount_ptr, alignment); + } + }, + Refcount.none => unreachable, + } + } +} + +pub fn isUnique( + bytes_or_null: ?[*]u8, +) callconv(.C) bool { + var bytes = bytes_or_null orelse return true; + + const ptr = @intFromPtr(bytes); + const tag_mask: usize = if (@sizeOf(usize) == 8) 0b111 else 0b11; + const masked_ptr = ptr & ~tag_mask; + + const isizes: [*]isize = @as([*]isize, @ptrFromInt(masked_ptr)); + + const refcount = (isizes - 1)[0]; + + if (DEBUG_INCDEC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("| is unique {*}\n", .{isizes - 1}); + } + + return refcount == REFCOUNT_ONE_ISIZE; +} + +// We follow roughly the [fbvector](https://github.com/facebook/folly/blob/main/folly/docs/FBVector.md) when it comes to growing a RocList. +// Here is [their growth strategy](https://github.com/facebook/folly/blob/3e0525988fd444201b19b76b390a5927c15cb697/folly/FBVector.h#L1128) for push_back: +// +// (1) initial size +// Instead of growing to size 1 from empty, fbvector allocates at least +// 64 bytes. You may still use reserve to reserve a lesser amount of +// memory. +// (2) 1.5x +// For medium-sized vectors, the growth strategy is 1.5x. See the docs +// for details. +// This does not apply to very small or very large fbvectors. This is a +// heuristic. +// +// In our case, we exposed allocate and reallocate, which will use a smart growth stategy. +// We also expose allocateExact and reallocateExact for case where a specific number of elements is requested. + +// calculateCapacity should only be called in cases the list will be growing. +// requested_length should always be greater than old_capacity. +pub inline fn calculateCapacity( + old_capacity: usize, + requested_length: usize, + element_width: usize, +) usize { + // TODO: there are two adjustments that would likely lead to better results for Roc. + // 1. Deal with the fact we allocate an extra u64 for refcount. + // This may lead to allocating page size + 8 bytes. + // That could mean allocating an entire page for 8 bytes of data which isn't great. + // 2. Deal with the fact that we can request more than 1 element at a time. + // fbvector assumes just appending 1 element at a time when using this algorithm. + // As such, they will generally grow in a way that should better match certain memory multiple. + // This is also the normal case for roc, but we could also grow by a much larger amount. + // We may want to round to multiples of 2 or something similar. + var new_capacity: usize = 0; + if (element_width == 0) { + return requested_length; + } else if (old_capacity == 0) { + new_capacity = 64 / element_width; + } else if (old_capacity < 4096 / element_width) { + new_capacity = old_capacity * 2; + } else if (old_capacity > 4096 * 32 / element_width) { + new_capacity = old_capacity * 2; + } else { + new_capacity = (old_capacity * 3 + 1) / 2; + } + + return @max(new_capacity, requested_length); +} + +pub fn allocateWithRefcountC( + data_bytes: usize, + element_alignment: u32, +) callconv(.C) [*]u8 { + return allocateWithRefcount(data_bytes, element_alignment); +} + +pub fn allocateWithRefcount( + data_bytes: usize, + element_alignment: u32, +) [*]u8 { + const ptr_width = @sizeOf(usize); + const alignment = @max(ptr_width, element_alignment); + const length = alignment + data_bytes; + + var new_bytes: [*]u8 = alloc(length, alignment) orelse unreachable; + + if (DEBUG_ALLOC and builtin.target.cpu.arch != .wasm32) { + std.debug.print("+ allocated {*} ({} bytes with alignment {})\n", .{ new_bytes, data_bytes, alignment }); + } + + const data_ptr = new_bytes + alignment; + const refcount_ptr = @as([*]usize, @ptrCast(@as([*]align(ptr_width) u8, @alignCast(data_ptr)) - ptr_width)); + refcount_ptr[0] = if (RC_TYPE == Refcount.none) REFCOUNT_MAX_ISIZE else REFCOUNT_ONE; + + return data_ptr; +} + +pub const CSlice = extern struct { + pointer: *anyopaque, + len: usize, +}; + +pub fn unsafeReallocate( + source_ptr: [*]u8, + alignment: u32, + old_length: usize, + new_length: usize, + element_width: usize, +) [*]u8 { + const align_width: usize = @max(alignment, @sizeOf(usize)); + + const old_width = align_width + old_length * element_width; + const new_width = align_width + new_length * element_width; + + if (old_width >= new_width) { + return source_ptr; + } + + // TODO handle out of memory + // NOTE realloc will dealloc the original allocation + const old_allocation = source_ptr - align_width; + const new_allocation = realloc(old_allocation, new_width, old_width, alignment); + + const new_source = @as([*]u8, @ptrCast(new_allocation)) + align_width; + return new_source; +} + +pub const Ordering = enum(u8) { + EQ = 0, + GT = 1, + LT = 2, +}; + +pub const UpdateMode = enum(u8) { + Immutable = 0, + InPlace = 1, +}; + +test "increfC, refcounted data" { + var mock_rc: isize = REFCOUNT_ONE_ISIZE + 17; + var ptr_to_refcount: *isize = &mock_rc; + increfRcPtrC(ptr_to_refcount, 2); + try std.testing.expectEqual(mock_rc, REFCOUNT_ONE_ISIZE + 19); +} + +test "increfC, static data" { + var mock_rc: isize = REFCOUNT_MAX_ISIZE; + var ptr_to_refcount: *isize = &mock_rc; + increfRcPtrC(ptr_to_refcount, 2); + try std.testing.expectEqual(mock_rc, REFCOUNT_MAX_ISIZE); +} + +// This returns a compilation dependent pseudo random seed for dictionaries. +// The seed is the address of this function. +// This avoids all roc Dicts using a known seed and being trivial to DOS. +// Still not as secure as true random, but a lot better. +// This value must not change between calls unless Dict is changed to store the seed on creation. +// Note: On esstentially all OSes, this will be affected by ASLR and different each run. +// In wasm, the value will be constant to the build as a whole. +// Either way, it can not be know by an attacker unless they get access to the executable. +pub fn dictPseudoSeed() callconv(.C) u64 { + return @as(u64, @intCast(@intFromPtr(&dictPseudoSeed))); +} diff --git a/examples/platform-switching/web-assembly-platform/host.zig b/examples/platform-switching/web-assembly/host/main.zig similarity index 97% rename from examples/platform-switching/web-assembly-platform/host.zig rename to examples/platform-switching/web-assembly/host/main.zig index b6babf362f0..54ed23a6a31 100644 --- a/examples/platform-switching/web-assembly-platform/host.zig +++ b/examples/platform-switching/web-assembly/host/main.zig @@ -1,4 +1,4 @@ -const str = @import("glue").str; +const str = @import("glue/str.zig"); const builtin = @import("builtin"); const RocStr = str.RocStr; diff --git a/examples/platform-switching/swift-platform/main.roc b/examples/platform-switching/web-assembly/platform/main.roc similarity index 85% rename from examples/platform-switching/swift-platform/main.roc rename to examples/platform-switching/web-assembly/platform/main.roc index a9eb403cd97..e141577dcb2 100644 --- a/examples/platform-switching/swift-platform/main.roc +++ b/examples/platform-switching/web-assembly/platform/main.roc @@ -1,4 +1,4 @@ -platform "echo-in-swift" +platform "" requires {} { main : Str } exposes [] packages {} diff --git a/examples/platform-switching/rocLovesWebAssembly.roc b/examples/platform-switching/web-assembly/rocLovesWebAssembly.roc similarity index 100% rename from examples/platform-switching/rocLovesWebAssembly.roc rename to examples/platform-switching/web-assembly/rocLovesWebAssembly.roc diff --git a/examples/platform-switching/web-assembly-platform/host.js b/examples/platform-switching/web-assembly/web/host.js similarity index 100% rename from examples/platform-switching/web-assembly-platform/host.js rename to examples/platform-switching/web-assembly/web/host.js diff --git a/examples/platform-switching/web-assembly-platform/host.test.js b/examples/platform-switching/web-assembly/web/host.test.js similarity index 100% rename from examples/platform-switching/web-assembly-platform/host.test.js rename to examples/platform-switching/web-assembly/web/host.test.js diff --git a/examples/platform-switching/web-assembly-platform/index.html b/examples/platform-switching/web-assembly/web/index.html similarity index 100% rename from examples/platform-switching/web-assembly-platform/index.html rename to examples/platform-switching/web-assembly/web/index.html diff --git a/examples/platform-switching/zig/.gitignore b/examples/platform-switching/zig/.gitignore new file mode 100644 index 00000000000..adf7a9b2742 --- /dev/null +++ b/examples/platform-switching/zig/.gitignore @@ -0,0 +1,6 @@ +host/glue/* +platform/*.a +zig-cache/ +zig-out/ +build +rocLovesZig \ No newline at end of file diff --git a/examples/platform-switching/zig/build.roc b/examples/platform-switching/zig/build.roc new file mode 100644 index 00000000000..1db8e9d73c6 --- /dev/null +++ b/examples/platform-switching/zig/build.roc @@ -0,0 +1,57 @@ +app "" + packages { + cli: "https://github.com/roc-lang/basic-cli/releases/download/0.10.0/vNe6s9hWzoTZtFmNkvEICPErI9ptji_ySjicO6CkucY.tar.br", + build: "../../../examples/build-helpers/main.roc", + } + imports [cli.Task.{ Task }, cli.Path, cli.Env, cli.Cmd, build.Help] + provides [main] to cli + +main = + + # use ENV VARs for easier automation from within test runner + roc = Env.var "ROC" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ROC" + glue = Env.var "ZIG_GLUE" |> Task.mapErr! \_ -> EnvionmentVariableNotSet "ZIG_GLUE" + + # get the current OS and ARCH + target = getTarget! + + # the prebuilt binary `macos-arm64.a` changes based on target + prebuiltBinaryPath = "platform/$(Help.prebuiltBinaryName target)" + + when Path.isFile (Path.fromStr prebuiltBinaryPath) |> Task.result! is + Ok _ -> + Task.ok {} # the prebuilt binary already exists... no need to rebuild + Err _ -> + # generate glue for the host + Cmd.exec roc ["glue", glue, "host/glue/", "platform/main.roc"] + |> Task.mapErr! ErrGeneratingGlue + + # build the host + Cmd.exec "zig" ["build"] + |> Task.mapErr! ErrBuildingHost + + # copy pre-built binary into platform + Cmd.exec "cp" ["-f", "zig-out/lib/libhost.a", prebuiltBinaryPath] + |> Task.mapErr! ErrCopyPrebuiltBinary + +getTarget : Task Help.RocTarget _ +getTarget = + + arch = + Cmd.new "uname" + |> Cmd.arg "-m" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.archFromStr + |> Task.mapErr! \err -> ErrGettingNativeArch (Inspect.toStr err) + + os = + Cmd.new "uname" + |> Cmd.arg "-s" + |> Cmd.output + |> Task.map .stdout + |> Task.map Help.osFromStr + |> Task.mapErr! \err -> ErrGettingNativeOS (Inspect.toStr err) + + Help.rocTarget { os, arch } |> Task.fromResult! + diff --git a/examples/platform-switching/zig/build.zig b/examples/platform-switching/zig/build.zig new file mode 100644 index 00000000000..93a3afde53e --- /dev/null +++ b/examples/platform-switching/zig/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const lib = b.addStaticLibrary(.{ + .name = "host", + .root_source_file = .{ .path = "host/main.zig" }, + .target = target, + .optimize = optimize, + }); + + b.installArtifact(lib); +} diff --git a/examples/platform-switching/zig-platform/host.zig b/examples/platform-switching/zig/host/main.zig similarity index 99% rename from examples/platform-switching/zig-platform/host.zig rename to examples/platform-switching/zig/host/main.zig index 27df6519182..574dedf62f7 100644 --- a/examples/platform-switching/zig-platform/host.zig +++ b/examples/platform-switching/zig/host/main.zig @@ -1,6 +1,6 @@ const std = @import("std"); const builtin = @import("builtin"); -const str = @import("glue").str; +const str = @import("glue/str.zig"); const RocStr = str.RocStr; const testing = std.testing; const expectEqual = testing.expectEqual; diff --git a/examples/platform-switching/zig/platform/main.roc b/examples/platform-switching/zig/platform/main.roc new file mode 100644 index 00000000000..e141577dcb2 --- /dev/null +++ b/examples/platform-switching/zig/platform/main.roc @@ -0,0 +1,9 @@ +platform "" + requires {} { main : Str } + exposes [] + packages {} + imports [] + provides [mainForHost] + +mainForHost : Str +mainForHost = main diff --git a/examples/platform-switching/zig/rocLovesZig.roc b/examples/platform-switching/zig/rocLovesZig.roc new file mode 100644 index 00000000000..2150c3b4748 --- /dev/null +++ b/examples/platform-switching/zig/rocLovesZig.roc @@ -0,0 +1,3 @@ +app [main] { pf: platform "platform/main.roc" } + +main = "Roc <3 Zig!\n"