diff --git a/cli/src/main.rs b/cli/src/main.rs index 8203304e2f..203d01b52d 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -24,6 +24,7 @@ use std::fs::File; use std::io; use std::io::prelude::*; use std::path::{Path, PathBuf}; +use std::process::Command; use target_lexicon::{Architecture, OperatingSystem, Triple, Vendor}; pub mod helpers; @@ -34,12 +35,19 @@ fn main() -> io::Result<()> { match argv.get(1) { Some(filename) => { - let mut file = File::open(filename)?; + let mut path = Path::new(filename).canonicalize().unwrap(); + + if !path.is_absolute() { + path = std::env::current_dir()?.join(path).canonicalize().unwrap(); + } + + // Step 1: build the .o file for the app + let mut file = File::open(path.clone())?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - let dest_filename = Path::new(filename).with_extension("o"); + let dest_filename = path.with_extension("o"); gen( Path::new(filename).to_path_buf(), @@ -50,7 +58,35 @@ fn main() -> io::Result<()> { let end_time = now.elapsed().unwrap(); - println!("Finished in {} ms\n", end_time.as_millis()); + println!( + "Finished compilation and code gen in {} ms\n", + end_time.as_millis() + ); + + let cwd = dest_filename.parent().unwrap(); + let lib_path = dest_filename.with_file_name("libroc_app.a"); + + // Step 2: turn the .o file into a .a static library + Command::new("ar") // TODO on Windows, use `link` + .args(&[ + "rcs", + lib_path.to_str().unwrap(), + dest_filename.to_str().unwrap(), + ]) + .spawn() + .expect("`ar` failed to run"); + + // Step 3: have rustc compile the host and link in the .a file + Command::new("rustc") + .args(&["-L", ".", "host.rs", "-o", "app"]) + .current_dir(cwd) + .spawn() + .expect("rustc failed to run"); + + // Step 4: Run the compiled app + Command::new(cwd.join("app")) + .spawn() + .expect("./app failed to run"); Ok(()) } diff --git a/examples/hello-world/.gitignore b/examples/hello-world/.gitignore index cea06e8f97..0bd05ae96a 100644 --- a/examples/hello-world/.gitignore +++ b/examples/hello-world/.gitignore @@ -1,4 +1,3 @@ -hello +app *.o -*.so *.a diff --git a/examples/hello-world/README.md b/examples/hello-world/README.md index b984d66659..d53e6e4251 100644 --- a/examples/hello-world/README.md +++ b/examples/hello-world/README.md @@ -1,27 +1,15 @@ # Hello, World! -Right now, there is only one way to build Roc programs: the Rube Goldberg Build Process. -(In the future, it will be nicer. At the moment, the nicer build system exists only -in our imaginations...so Rube Goldberg it is!) +To run: -## Ingredients - -1. A host. For this example, our host is implemented in the file `host.rs`. -2. A Roc function. For this example, we'll use a function which returns the Roc string `"Hello, World!"` -3. Having `gcc` installed. This will not be necessary in the future, but the Rube Goldberg build process needs it. - -## Steps - -1. `cd` into `examples/hello-world/` -2. Run `cargo run hello.roc` to compile the Roc source code into a `hello.o` file. -3. Run `ar rcs libhello_from_roc.a hello.o` to generate `libhello_from_roc.a`. (This filename must begin with `lib` and end in `.a` or else `host.rs` won't be able to find it!) -4. Run `rustc -L . host.rs -o hello` to generate the `hello` executable. -5. Run `./hello` to see the greeting! +```bash +$ cargo run hello.roc +``` To run in release mode instead, do: ```bash -cargo run --release hello.roc +$ cargo run --release hello.roc ``` ## Design Notes @@ -55,177 +43,3 @@ boundary. This not only gets us host compatibility with C compilers, but also Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) generates correct Rust FFI bindings from C headers. -The whole "calling gcc and rustc" part of the current build process is obviously -not something Roc application authors should ever need to do. Rather, the idea -would be to have the host precompiled into an object file (eliminating the -need for Roc authors to run `rustc` in this example) and for the Roc compiler -to not only generate the object file for the Roc file, but also to link it with -the host object file to produce an executable (eliminating the need for `gcc`) -such that Roc application authors can concern themselves exclusively with Roc code -and need only the Roc compiler to build and to execute it. - -Of course, none of those niceties exist yet. But we'll get there! - -## The test that builds things - -```rust -let src = indoc!( - r#" - "Hello, World!" - "# -); - -// Build the expr -let arena = Bump::new(); -let (loc_expr, _output, _problems, subs, var, constraint, home, interns) = uniq_expr(src); - -let mut unify_problems = Vec::new(); -let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var); - -let context = Context::create(); -let module = context.create_module("app"); -let builder = context.create_builder(); -let fpm = PassManager::create(&module); - -roc_gen::llvm::build::add_passes(&fpm); - -fpm.initialize(); - -// Compute main_fn_type before moving subs to Env -let layout = Layout::from_content(&arena, content, &subs, crate::helpers::eval::POINTER_SIZE) -.unwrap_or_else(|err| panic!("Code gen error in test: could not convert to layout. Err was {:?} and Subs were {:?}", err, subs)); - -let execution_engine = module - .create_jit_execution_engine(OptimizationLevel::None) - .expect("Error creating JIT execution engine for test"); - -let ptr_bytes = execution_engine - .get_target_data() - .get_pointer_byte_size(None); -let main_fn_type = - basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false); -let main_fn_name = "$Test.main"; - -// Compile and add all the Procs before adding main -let mut env = roc_gen::llvm::build::Env { - arena: &arena, - builder: &builder, - context: &context, - interns, - module: arena.alloc(module), - ptr_bytes, -}; -let mut procs = Procs::default(); -let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap(); - -// Populate Procs and get the low-level Expr from the canonical Expr -let main_body = Expr::new( - &arena, - &mut subs, - loc_expr.value, - &mut procs, - home, - &mut ident_ids, - crate::helpers::eval::POINTER_SIZE, -); - -// Put this module's ident_ids back in the interns, so we can use them in env. -env.interns.all_ident_ids.insert(home, ident_ids); - -let mut headers = Vec::with_capacity(procs.len()); - -// Add all the Proc headers to the module. -// We have to do this in a separate pass first, -// because their bodies may reference each other. -for (symbol, opt_proc) in procs.as_map().into_iter() { - if let Some(proc) = opt_proc { - let (fn_val, arg_basic_types) = build_proc_header(&env, symbol, &proc); - - headers.push((proc, fn_val, arg_basic_types)); - } -} - -// Build each proc using its header info. -for (proc, fn_val, arg_basic_types) in headers { - // NOTE: This is here to be uncommented in case verification fails. - // (This approach means we don't have to defensively clone name here.) - // - // println!("\n\nBuilding and then verifying function {}\n\n", name); - build_proc(&env, proc, &procs, fn_val, arg_basic_types); - - if fn_val.verify(true) { - fpm.run_on(&fn_val); - } else { - // NOTE: If this fails, uncomment the above println to debug. - panic!("Non-main function failed LLVM verification. Uncomment the above println to debug!"); - } -} - -// Add main to the module. -let main_fn = env.module.add_function(main_fn_name, main_fn_type, None); - -main_fn.set_call_conventions(crate::helpers::eval::MAIN_CALLING_CONVENTION); -main_fn.set_linkage(Linkage::External); - -// Add main's body -let basic_block = context.append_basic_block(main_fn, "entry"); - -builder.position_at_end(basic_block); - -let ret = roc_gen::llvm::build::build_expr( - &env, - &ImMap::default(), - main_fn, - &main_body, - &mut Procs::default(), -); - -builder.build_return(Some(&ret)); - -// Uncomment this to see the module's un-optimized LLVM instruction output: -// env.module.print_to_stderr(); - -if main_fn.verify(true) { - fpm.run_on(&main_fn); -} else { - panic!("Function {} failed LLVM verification.", main_fn_name); -} - -// Verify the module -if let Err(errors) = env.module.verify() { - panic!("Errors defining module: {:?}", errors); -} - -// Uncomment this to see the module's optimized LLVM instruction output: -// env.module.print_to_stderr(); - -// Emit -Target::initialize_x86(&InitializationConfig::default()); - -let opt = OptimizationLevel::Default; -let reloc = RelocMode::Default; -let model = CodeModel::Default; -let target = Target::from_name("x86-64").unwrap(); -let target_machine = target - .create_target_machine( - &TargetTriple::create("x86_64-pc-linux-gnu"), - "x86-64", - "+avx2", - opt, - reloc, - model, - ) - .unwrap(); - -let path = Path::new("../../hello.o"); - -assert!(target_machine - .write_to_file(&env.module, FileType::Object, &path) - .is_ok()); - -let path = Path::new("../../hello.asm"); - -assert!(target_machine - .write_to_file(&env.module, FileType::Assembly, &path) - .is_ok()); -``` diff --git a/examples/hello-world/build.sh b/examples/hello-world/build.sh deleted file mode 100755 index 361cce70d1..0000000000 --- a/examples/hello-world/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -Eeuo pipefail - -cargo run hello.roc -ar rcs libhello_from_roc.a hello.o -rustc -L . host.rs -o hello - -./hello diff --git a/examples/hello-world/host.rs b/examples/hello-world/host.rs index 33683fe3be..eb3702b810 100644 --- a/examples/hello-world/host.rs +++ b/examples/hello-world/host.rs @@ -1,7 +1,7 @@ use std::ffi::CStr; use std::os::raw::c_char; -#[link(name = "hello_from_roc", kind = "static")] +#[link(name = "roc_app", kind = "static")] extern "C" { #[link_name = "$Test.main"] fn str_from_roc() -> *const c_char; diff --git a/examples/quicksort/.gitignore b/examples/quicksort/.gitignore index e5021428ef..0bd05ae96a 100644 --- a/examples/quicksort/.gitignore +++ b/examples/quicksort/.gitignore @@ -1,4 +1,3 @@ -qs +app *.o -*.so *.a diff --git a/examples/quicksort/README.md b/examples/quicksort/README.md index 9cedb802b7..859f54dfcc 100644 --- a/examples/quicksort/README.md +++ b/examples/quicksort/README.md @@ -1,234 +1,13 @@ # Quicksort -Right now, there is only one way to build Roc programs: the Rube Goldberg Build Process. -(In the future, it will be nicer. At the moment, the nicer build system exists only -in our imaginations...so Rube Goldberg it is!) - -*NOTE:* On macOS or Linux, you can run `./build.sh` from this directory instead of following these instructions. - -## Ingredients - -1. A host. For this example, our host is implemented in the file `host.rs`. -2. A Roc function. For this example, we'll use a function which returns a list of integers. -3. Having `gcc` installed. This will not be necessary in the future, but the Rube Goldberg build process needs it. - -## Steps - -1. `cd` into `examples/quicksort/` -2. Run `cargo run qs.roc` to compile the Roc source code into a `qs.o` file. -3. Run `ar rcs libroc_qs_main.a qs.o` to generate `libroc_qs_main.a`. (This filename must begin with `lib` and end in `.a` or else `host.rs` won't be able to find it!) -4. Run `rustc -L . host.rs -o qs` to generate the `qs` executable. -5. Run `./qs` to see the output! +To run: +```bash +$ cargo run qs.roc +``` To run in release mode instead, do: ```bash -cargo run --release qs.roc -``` - -## Design Notes - -This demonstrates the basic design of hosts: Roc code gets compiled into a pure -function (in this case, a thunk that always returns `"Hello, World!"`) and -then the host calls that function. Fundamentally, that's the whole idea! The host -might not even have a `main` - it could be a library, a plugin, anything. -Everything else is built on this basic "hosts calling linked pure functions" design. - -For example, things get more interesting when the compiled Roc function returns -a `Task` - that is, a tagged union data structure containing function pointers -to callback closures. This lets the Roc pure function describe arbitrary -chainable effects, which the host can interpret to perform I/O as requested by -the Roc program. (The tagged union `Task` would have a variant for each supported -I/O operation.) - -In this trivial example, it's very easy to line up the API between the host and -the Roc program. In a more involved host, this would be much trickier - especially -if the API were changing frequently during development. - -The idea there is to have a first-class concept of "glue code" which host authors -can write (it would be plain Roc code, but with some extra keywords that aren't -available in normal modules - kinda like `port module` in Elm), and which -describe both the Roc-host/C boundary as well as the Roc-host/Roc-app boundary. -Roc application authors only care about the Roc-host/Roc-app portion, and the -host author only cares about the Roc-host/C bounary when implementing the host. - -Using this glue code, the Roc compiler can generate C header files describing the -boundary. This not only gets us host compatibility with C compilers, but also -Rust FFI for free, because [`rust-bindgen`](https://github.com/rust-lang/rust-bindgen) -generates correct Rust FFI bindings from C headers. - -The whole "calling gcc and rustc" part of the current build process is obviously -not something Roc application authors should ever need to do. Rather, the idea -would be to have the host precompiled into an object file (eliminating the -need for Roc authors to run `rustc` in this example) and for the Roc compiler -to not only generate the object file for the Roc file, but also to link it with -the host object file to produce an executable (eliminating the need for `gcc`) -such that Roc application authors can concern themselves exclusively with Roc code -and need only the Roc compiler to build and to execute it. - -Of course, none of those niceties exist yet. But we'll get there! - -## The test that builds things - -```rust -let src = indoc!( - r#" - "Hello, World!" - "# -); - -// Build the expr -let arena = Bump::new(); -let (loc_expr, _output, _problems, subs, var, constraint, home, interns) = uniq_expr(src); - -let mut unify_problems = Vec::new(); -let (content, mut subs) = infer_expr(subs, &mut unify_problems, &constraint, var); - -let context = Context::create(); -let module = context.create_module("app"); -let builder = context.create_builder(); -let fpm = PassManager::create(&module); - -roc_gen::llvm::build::add_passes(&fpm); - -fpm.initialize(); - -// Compute main_fn_type before moving subs to Env -let layout = Layout::from_content(&arena, content, &subs, crate::helpers::eval::POINTER_SIZE) -.unwrap_or_else(|err| panic!("Code gen error in test: could not convert to layout. Err was {:?} and Subs were {:?}", err, subs)); - -let execution_engine = module - .create_jit_execution_engine(OptimizationLevel::None) - .expect("Error creating JIT execution engine for test"); - -let ptr_bytes = execution_engine - .get_target_data() - .get_pointer_byte_size(None); -let main_fn_type = - basic_type_from_layout(&arena, &context, &layout, ptr_bytes).fn_type(&[], false); -let main_fn_name = "$Test.main"; - -// Compile and add all the Procs before adding main -let mut env = roc_gen::llvm::build::Env { - arena: &arena, - builder: &builder, - context: &context, - interns, - module: arena.alloc(module), - ptr_bytes, -}; -let mut procs = Procs::default(); -let mut ident_ids = env.interns.all_ident_ids.remove(&home).unwrap(); - -// Populate Procs and get the low-level Expr from the canonical Expr -let main_body = Expr::new( - &arena, - &mut subs, - loc_expr.value, - &mut procs, - home, - &mut ident_ids, - crate::helpers::eval::POINTER_SIZE, -); - -// Put this module's ident_ids back in the interns, so we can use them in env. -env.interns.all_ident_ids.insert(home, ident_ids); - -let mut headers = Vec::with_capacity(procs.len()); - -// Add all the Proc headers to the module. -// We have to do this in a separate pass first, -// because their bodies may reference each other. -for (symbol, opt_proc) in procs.as_map().into_iter() { - if let Some(proc) = opt_proc { - let (fn_val, arg_basic_types) = build_proc_header(&env, symbol, &proc); - - headers.push((proc, fn_val, arg_basic_types)); - } -} - -// Build each proc using its header info. -for (proc, fn_val, arg_basic_types) in headers { - // NOTE: This is here to be uncommented in case verification fails. - // (This approach means we don't have to defensively clone name here.) - // - // println!("\n\nBuilding and then verifying function {}\n\n", name); - build_proc(&env, proc, &procs, fn_val, arg_basic_types); - - if fn_val.verify(true) { - fpm.run_on(&fn_val); - } else { - // NOTE: If this fails, uncomment the above println to debug. - panic!("Non-main function failed LLVM verification. Uncomment the above println to debug!"); - } -} - -// Add main to the module. -let main_fn = env.module.add_function(main_fn_name, main_fn_type, None); - -main_fn.set_call_conventions(crate::helpers::eval::MAIN_CALLING_CONVENTION); -main_fn.set_linkage(Linkage::External); - -// Add main's body -let basic_block = context.append_basic_block(main_fn, "entry"); - -builder.position_at_end(basic_block); - -let ret = roc_gen::llvm::build::build_expr( - &env, - &ImMap::default(), - main_fn, - &main_body, - &mut Procs::default(), -); - -builder.build_return(Some(&ret)); - -// Uncomment this to see the module's un-optimized LLVM instruction output: -// env.module.print_to_stderr(); - -if main_fn.verify(true) { - fpm.run_on(&main_fn); -} else { - panic!("Function {} failed LLVM verification.", main_fn_name); -} - -// Verify the module -if let Err(errors) = env.module.verify() { - panic!("Errors defining module: {:?}", errors); -} - -// Uncomment this to see the module's optimized LLVM instruction output: -// env.module.print_to_stderr(); - -// Emit -Target::initialize_x86(&InitializationConfig::default()); - -let opt = OptimizationLevel::Default; -let reloc = RelocMode::Default; -let model = CodeModel::Default; -let target = Target::from_name("x86-64").unwrap(); -let target_machine = target - .create_target_machine( - &TargetTriple::create("x86_64-pc-linux-gnu"), - "x86-64", - "+avx2", - opt, - reloc, - model, - ) - .unwrap(); - -let path = Path::new("../../hello.o"); - -assert!(target_machine - .write_to_file(&env.module, FileType::Object, &path) - .is_ok()); - -let path = Path::new("../../hello.asm"); - -assert!(target_machine - .write_to_file(&env.module, FileType::Assembly, &path) - .is_ok()); +$ cargo run --release qs.roc ``` diff --git a/examples/quicksort/build.sh b/examples/quicksort/build.sh deleted file mode 100755 index 15eb8b8e92..0000000000 --- a/examples/quicksort/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -set -Eeuo pipefail - -cargo run qs.roc -ar rcs libroc_qs_main.a qs.o -rustc -L . host.rs -o qs - -./qs diff --git a/examples/quicksort/host.rs b/examples/quicksort/host.rs index 30e1690589..9a55e3dcd8 100644 --- a/examples/quicksort/host.rs +++ b/examples/quicksort/host.rs @@ -1,4 +1,4 @@ -#[link(name = "roc_qs_main", kind = "static")] +#[link(name = "roc_app", kind = "static")] extern "C" { #[allow(improper_ctypes)] #[link_name = "$Test.main"]