mirror of
https://github.com/roc-lang/roc.git
synced 2025-11-03 14:12:53 +00:00
529 lines
16 KiB
Rust
529 lines
16 KiB
Rust
use bumpalo::Bump;
|
|
use roc_build::{
|
|
link::{link, preprocess_host_wasm32, rebuild_host, LinkType, LinkingStrategy},
|
|
program::{self, CodeGenOptions, Problems},
|
|
};
|
|
use roc_builtins::bitcode;
|
|
use roc_load::{
|
|
EntryPoint, ExecutionMode, ExpectMetadata, LoadConfig, LoadMonomorphizedError, LoadedModule,
|
|
LoadingProblem, Threading,
|
|
};
|
|
use roc_mono::ir::OptLevel;
|
|
use roc_reporting::report::{RenderTarget, DEFAULT_PALETTE};
|
|
use roc_target::TargetInfo;
|
|
use std::time::{Duration, Instant};
|
|
use std::{path::PathBuf, thread::JoinHandle};
|
|
use target_lexicon::Triple;
|
|
use tempfile::Builder;
|
|
|
|
fn report_timing(buf: &mut String, label: &str, duration: Duration) {
|
|
use std::fmt::Write;
|
|
|
|
writeln!(
|
|
buf,
|
|
" {:9.3} ms {}",
|
|
duration.as_secs_f64() * 1000.0,
|
|
label,
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
pub struct BuiltFile<'a> {
|
|
pub binary_path: PathBuf,
|
|
pub problems: Problems,
|
|
pub total_time: Duration,
|
|
pub expect_metadata: ExpectMetadata<'a>,
|
|
}
|
|
|
|
pub enum BuildOrdering {
|
|
/// Run up through typechecking first; continue building iff that is successful.
|
|
BuildIfChecks,
|
|
/// Always build the Roc binary, even if there are type errors.
|
|
AlwaysBuild,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub enum BuildFileError<'a> {
|
|
LoadingProblem(LoadingProblem<'a>),
|
|
ErrorModule {
|
|
module: LoadedModule,
|
|
total_time: Duration,
|
|
},
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn build_file<'a>(
|
|
arena: &'a Bump,
|
|
target: &Triple,
|
|
app_module_path: PathBuf,
|
|
code_gen_options: CodeGenOptions,
|
|
emit_timings: bool,
|
|
link_type: LinkType,
|
|
linking_strategy: LinkingStrategy,
|
|
prebuilt: bool,
|
|
threading: Threading,
|
|
wasm_dev_stack_bytes: Option<u32>,
|
|
order: BuildOrdering,
|
|
) -> Result<BuiltFile<'a>, BuildFileError<'a>> {
|
|
let compilation_start = Instant::now();
|
|
let target_info = TargetInfo::from(target);
|
|
|
|
// Step 1: compile the app and generate the .o file
|
|
let subs_by_module = Default::default();
|
|
|
|
let exec_mode = match order {
|
|
BuildOrdering::BuildIfChecks => ExecutionMode::ExecutableIfCheck,
|
|
BuildOrdering::AlwaysBuild => ExecutionMode::Executable,
|
|
};
|
|
|
|
let load_config = LoadConfig {
|
|
target_info,
|
|
// TODO: expose this from CLI?
|
|
render: RenderTarget::ColorTerminal,
|
|
palette: DEFAULT_PALETTE,
|
|
threading,
|
|
exec_mode,
|
|
};
|
|
let load_result = roc_load::load_and_monomorphize(
|
|
arena,
|
|
app_module_path.clone(),
|
|
subs_by_module,
|
|
load_config,
|
|
);
|
|
let loaded = match load_result {
|
|
Ok(loaded) => loaded,
|
|
Err(LoadMonomorphizedError::LoadingProblem(problem)) => {
|
|
return Err(BuildFileError::LoadingProblem(problem))
|
|
}
|
|
Err(LoadMonomorphizedError::ErrorModule(module)) => {
|
|
return Err(BuildFileError::ErrorModule {
|
|
module,
|
|
total_time: compilation_start.elapsed(),
|
|
})
|
|
}
|
|
};
|
|
|
|
use target_lexicon::Architecture;
|
|
let emit_wasm = matches!(target.architecture, Architecture::Wasm32);
|
|
|
|
// TODO wasm host extension should be something else ideally
|
|
// .bc does not seem to work because
|
|
//
|
|
// > Non-Emscripten WebAssembly hasn't implemented __builtin_return_address
|
|
//
|
|
// and zig does not currently emit `.a` webassembly static libraries
|
|
let (host_extension, app_extension, extension) = {
|
|
use roc_target::OperatingSystem::*;
|
|
|
|
match roc_target::OperatingSystem::from(target.operating_system) {
|
|
Wasi => {
|
|
if matches!(code_gen_options.opt_level, OptLevel::Development) {
|
|
("wasm", "wasm", Some("wasm"))
|
|
} else {
|
|
("zig", "bc", Some("wasm"))
|
|
}
|
|
}
|
|
Unix => ("o", "o", None),
|
|
Windows => ("obj", "obj", Some("exe")),
|
|
}
|
|
};
|
|
|
|
let cwd = app_module_path.parent().unwrap();
|
|
let mut binary_path = cwd.join(&*loaded.output_path);
|
|
|
|
if let Some(extension) = extension {
|
|
binary_path.set_extension(extension);
|
|
}
|
|
|
|
let host_input_path = if let EntryPoint::Executable { platform_path, .. } = &loaded.entry_point
|
|
{
|
|
cwd.join(platform_path)
|
|
.with_file_name("host")
|
|
.with_extension(host_extension)
|
|
} else {
|
|
unreachable!();
|
|
};
|
|
|
|
// 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 exposed_values = loaded
|
|
.exposed_to_host
|
|
.values
|
|
.keys()
|
|
.map(|x| x.as_str(&loaded.interns).to_string())
|
|
.collect();
|
|
|
|
let exposed_closure_types = loaded
|
|
.exposed_to_host
|
|
.closure_types
|
|
.iter()
|
|
.map(|x| {
|
|
format!(
|
|
"{}_{}",
|
|
x.module_string(&loaded.interns),
|
|
x.as_str(&loaded.interns)
|
|
)
|
|
})
|
|
.collect();
|
|
|
|
let preprocessed_host_path = if emit_wasm {
|
|
host_input_path.with_file_name("preprocessedhost.o")
|
|
} else {
|
|
host_input_path.with_file_name("preprocessedhost")
|
|
};
|
|
|
|
let rebuild_thread = spawn_rebuild_thread(
|
|
code_gen_options.opt_level,
|
|
linking_strategy,
|
|
prebuilt,
|
|
host_input_path.clone(),
|
|
preprocessed_host_path.clone(),
|
|
binary_path.clone(),
|
|
target,
|
|
exposed_values,
|
|
exposed_closure_types,
|
|
);
|
|
|
|
let buf = &mut String::with_capacity(1024);
|
|
|
|
let mut it = loaded.timings.iter().peekable();
|
|
while let Some((module_id, module_timing)) = it.next() {
|
|
let module_name = loaded.interns.module_name(*module_id);
|
|
|
|
buf.push_str(" ");
|
|
|
|
if module_name.is_empty() {
|
|
// the App module
|
|
buf.push_str("Application Module");
|
|
} else {
|
|
buf.push_str(module_name);
|
|
}
|
|
|
|
buf.push('\n');
|
|
|
|
report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file);
|
|
report_timing(buf, "Parse header", module_timing.parse_header);
|
|
report_timing(buf, "Parse body", module_timing.parse_body);
|
|
report_timing(buf, "Canonicalize", module_timing.canonicalize);
|
|
report_timing(buf, "Constrain", module_timing.constrain);
|
|
report_timing(buf, "Solve", module_timing.solve);
|
|
report_timing(
|
|
buf,
|
|
"Find Specializations",
|
|
module_timing.find_specializations,
|
|
);
|
|
let multiple_make_specializations_passes = module_timing.make_specializations.len() > 1;
|
|
for (i, pass_time) in module_timing.make_specializations.iter().enumerate() {
|
|
let suffix = if multiple_make_specializations_passes {
|
|
format!(" (Pass {})", i)
|
|
} else {
|
|
String::new()
|
|
};
|
|
report_timing(buf, &format!("Make Specializations{}", suffix), *pass_time);
|
|
}
|
|
report_timing(buf, "Other", module_timing.other());
|
|
buf.push('\n');
|
|
report_timing(buf, "Total", module_timing.total());
|
|
|
|
if it.peek().is_some() {
|
|
buf.push('\n');
|
|
}
|
|
}
|
|
|
|
// This only needs to be mutable for report_problems. This can't be done
|
|
// inside a nested scope without causing a borrow error!
|
|
let mut loaded = loaded;
|
|
let problems = program::report_problems_monomorphized(&mut loaded);
|
|
let loaded = loaded;
|
|
|
|
enum HostRebuildTiming {
|
|
BeforeApp(u128),
|
|
ConcurrentWithApp(JoinHandle<u128>),
|
|
}
|
|
|
|
let rebuild_timing = if linking_strategy == LinkingStrategy::Additive {
|
|
let rebuild_duration = rebuild_thread
|
|
.join()
|
|
.expect("Failed to (re)build platform.");
|
|
if emit_timings && !prebuilt {
|
|
println!(
|
|
"Finished rebuilding the platform in {} ms\n",
|
|
rebuild_duration
|
|
);
|
|
}
|
|
HostRebuildTiming::BeforeApp(rebuild_duration)
|
|
} else {
|
|
HostRebuildTiming::ConcurrentWithApp(rebuild_thread)
|
|
};
|
|
|
|
let (roc_app_bytes, code_gen_timing, expect_metadata) = program::gen_from_mono_module(
|
|
arena,
|
|
loaded,
|
|
&app_module_path,
|
|
target,
|
|
code_gen_options,
|
|
&preprocessed_host_path,
|
|
wasm_dev_stack_bytes,
|
|
);
|
|
|
|
buf.push('\n');
|
|
buf.push_str(" ");
|
|
buf.push_str("Code Generation");
|
|
buf.push('\n');
|
|
|
|
report_timing(
|
|
buf,
|
|
"Generate Assembly from Mono IR",
|
|
code_gen_timing.code_gen,
|
|
);
|
|
|
|
let compilation_end = compilation_start.elapsed();
|
|
|
|
let size = roc_app_bytes.len();
|
|
|
|
if emit_timings {
|
|
println!(
|
|
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
|
|
buf
|
|
);
|
|
|
|
println!(
|
|
"Finished compilation and code gen in {} ms\n\nProduced a app.o file of size {:?}\n",
|
|
compilation_end.as_millis(),
|
|
size,
|
|
);
|
|
}
|
|
|
|
if let HostRebuildTiming::ConcurrentWithApp(thread) = rebuild_timing {
|
|
let rebuild_duration = thread.join().expect("Failed to (re)build platform.");
|
|
if emit_timings && !prebuilt {
|
|
println!(
|
|
"Finished rebuilding the platform in {} ms\n",
|
|
rebuild_duration
|
|
);
|
|
}
|
|
}
|
|
|
|
// Step 2: link the prebuilt platform and compiled app
|
|
let link_start = Instant::now();
|
|
let problems = match (linking_strategy, link_type) {
|
|
(LinkingStrategy::Surgical, _) => {
|
|
roc_linker::link_preprocessed_host(
|
|
target,
|
|
&host_input_path,
|
|
&roc_app_bytes,
|
|
&binary_path,
|
|
);
|
|
|
|
problems
|
|
}
|
|
(LinkingStrategy::Additive, _) | (LinkingStrategy::Legacy, LinkType::None) => {
|
|
// Just copy the object file to the output folder.
|
|
binary_path.set_extension(app_extension);
|
|
std::fs::write(&binary_path, &*roc_app_bytes).unwrap();
|
|
problems
|
|
}
|
|
(LinkingStrategy::Legacy, _) => {
|
|
let app_o_file = Builder::new()
|
|
.prefix("roc_app")
|
|
.suffix(&format!(".{}", app_extension))
|
|
.tempfile()
|
|
.map_err(|err| todo!("TODO Gracefully handle tempfile creation error {:?}", err))?;
|
|
let app_o_file = app_o_file.path();
|
|
|
|
std::fs::write(app_o_file, &*roc_app_bytes).unwrap();
|
|
|
|
let mut inputs = vec![
|
|
host_input_path.as_path().to_str().unwrap(),
|
|
app_o_file.to_str().unwrap(),
|
|
];
|
|
|
|
let builtins_host_file = tempfile::NamedTempFile::new().unwrap();
|
|
std::fs::write(builtins_host_file.path(), bitcode::HOST_UNIX)
|
|
.expect("failed to write host builtins object to tempfile");
|
|
|
|
if matches!(code_gen_options.backend, program::CodeGenBackend::Assembly) {
|
|
inputs.push(builtins_host_file.path().to_str().unwrap());
|
|
}
|
|
|
|
let (mut child, _) = // TODO use lld
|
|
link(target, binary_path.clone(), &inputs, link_type)
|
|
.map_err(|_| todo!("gracefully handle `ld` failing to spawn."))?;
|
|
|
|
let exit_status = child
|
|
.wait()
|
|
.map_err(|_| todo!("gracefully handle error after `ld` spawned"))?;
|
|
|
|
if exit_status.success() {
|
|
problems
|
|
} else {
|
|
let mut problems = problems;
|
|
|
|
// Add an error for `ld` failing
|
|
problems.errors += 1;
|
|
|
|
problems
|
|
}
|
|
}
|
|
};
|
|
|
|
let linking_time = link_start.elapsed();
|
|
|
|
if emit_timings {
|
|
println!("Finished linking in {} ms\n", linking_time.as_millis());
|
|
}
|
|
|
|
let total_time = compilation_start.elapsed();
|
|
|
|
Ok(BuiltFile {
|
|
binary_path,
|
|
problems,
|
|
total_time,
|
|
expect_metadata,
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
fn spawn_rebuild_thread(
|
|
opt_level: OptLevel,
|
|
linking_strategy: LinkingStrategy,
|
|
prebuilt: bool,
|
|
host_input_path: PathBuf,
|
|
preprocessed_host_path: PathBuf,
|
|
binary_path: PathBuf,
|
|
target: &Triple,
|
|
exported_symbols: Vec<String>,
|
|
exported_closure_types: Vec<String>,
|
|
) -> std::thread::JoinHandle<u128> {
|
|
let thread_local_target = target.clone();
|
|
std::thread::spawn(move || {
|
|
if !prebuilt {
|
|
// 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();
|
|
|
|
if !prebuilt {
|
|
match linking_strategy {
|
|
LinkingStrategy::Additive => {
|
|
let host_dest = rebuild_host(
|
|
opt_level,
|
|
&thread_local_target,
|
|
host_input_path.as_path(),
|
|
None,
|
|
);
|
|
|
|
preprocess_host_wasm32(host_dest.as_path(), &preprocessed_host_path);
|
|
}
|
|
LinkingStrategy::Surgical => {
|
|
roc_linker::build_and_preprocess_host(
|
|
opt_level,
|
|
&thread_local_target,
|
|
host_input_path.as_path(),
|
|
preprocessed_host_path.as_path(),
|
|
exported_symbols,
|
|
exported_closure_types,
|
|
);
|
|
}
|
|
LinkingStrategy::Legacy => {
|
|
rebuild_host(
|
|
opt_level,
|
|
&thread_local_target,
|
|
host_input_path.as_path(),
|
|
None,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
if linking_strategy == LinkingStrategy::Surgical {
|
|
// Copy preprocessed host to executable location.
|
|
std::fs::copy(preprocessed_host_path, binary_path.as_path()).unwrap();
|
|
}
|
|
let rebuild_host_end = rebuild_host_start.elapsed();
|
|
|
|
rebuild_host_end.as_millis()
|
|
})
|
|
}
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn check_file(
|
|
arena: &Bump,
|
|
roc_file_path: PathBuf,
|
|
emit_timings: bool,
|
|
threading: Threading,
|
|
) -> Result<(program::Problems, Duration), LoadingProblem> {
|
|
let compilation_start = Instant::now();
|
|
|
|
// only used for generating errors. We don't do code generation, so hardcoding should be fine
|
|
// we need monomorphization for when exhaustiveness checking
|
|
let target_info = TargetInfo::default_x86_64();
|
|
|
|
// Step 1: compile the app and generate the .o file
|
|
let subs_by_module = Default::default();
|
|
|
|
let load_config = LoadConfig {
|
|
target_info,
|
|
// TODO: expose this from CLI?
|
|
render: RenderTarget::ColorTerminal,
|
|
palette: DEFAULT_PALETTE,
|
|
threading,
|
|
exec_mode: ExecutionMode::Check,
|
|
};
|
|
let mut loaded =
|
|
roc_load::load_and_typecheck(arena, roc_file_path, subs_by_module, load_config)?;
|
|
|
|
let buf = &mut String::with_capacity(1024);
|
|
|
|
let mut it = loaded.timings.iter().peekable();
|
|
while let Some((module_id, module_timing)) = it.next() {
|
|
let module_name = loaded.interns.module_name(*module_id);
|
|
|
|
buf.push_str(" ");
|
|
|
|
if module_name.is_empty() {
|
|
// the App module
|
|
buf.push_str("Application Module");
|
|
} else {
|
|
buf.push_str(module_name);
|
|
}
|
|
|
|
buf.push('\n');
|
|
|
|
report_timing(buf, "Read .roc file from disk", module_timing.read_roc_file);
|
|
report_timing(buf, "Parse header", module_timing.parse_header);
|
|
report_timing(buf, "Parse body", module_timing.parse_body);
|
|
report_timing(buf, "Canonicalize", module_timing.canonicalize);
|
|
report_timing(buf, "Constrain", module_timing.constrain);
|
|
report_timing(buf, "Solve", module_timing.solve);
|
|
report_timing(buf, "Other", module_timing.other());
|
|
buf.push('\n');
|
|
report_timing(buf, "Total", module_timing.total());
|
|
|
|
if it.peek().is_some() {
|
|
buf.push('\n');
|
|
}
|
|
}
|
|
|
|
let compilation_end = compilation_start.elapsed();
|
|
|
|
if emit_timings {
|
|
println!(
|
|
"\n\nCompilation finished!\n\nHere's how long each module took to compile:\n\n{}",
|
|
buf
|
|
);
|
|
|
|
println!("Finished checking in {} ms\n", compilation_end.as_millis(),);
|
|
}
|
|
|
|
Ok((
|
|
program::report_problems_typechecked(&mut loaded),
|
|
compilation_end,
|
|
))
|
|
}
|