diff --git a/solx-codegen-evm/src/codegen/context/mod.rs b/solx-codegen-evm/src/codegen/context/mod.rs index 542a7da50..045d3dbf7 100644 --- a/solx-codegen-evm/src/codegen/context/mod.rs +++ b/solx-codegen-evm/src/codegen/context/mod.rs @@ -163,6 +163,37 @@ impl<'ctx> Context<'ctx> { ) -> anyhow::Result { let contract_path = self.module.get_name().to_str().expect("Always valid"); + let diagnostics = crate::diagnostics::Capture::install(self.llvm); + + // Per-unit codegen parameters ride the module as flags. The presence + // guards keep the first pass's values through the size-fallback + // recursion, whose settings do not carry them. + if let Some(size) = self.optimizer.settings().spill_area_size() + && self.module().get_flag("evm-stack-region-size").is_none() + { + self.module().add_basic_value_flag( + "evm-stack-region-offset", + inkwell::module::FlagBehavior::Error, + self.llvm + .i64_type() + .const_int(crate::r#const::SOLC_USER_MEMORY_OFFSET, false), + ); + self.module().add_basic_value_flag( + "evm-stack-region-size", + inkwell::module::FlagBehavior::Error, + self.llvm.i64_type().const_int(size, false), + ); + } + if let Some(size) = self.optimizer.settings().metadata_size + && self.module().get_flag("evm-metadata-size").is_none() + { + self.module().add_basic_value_flag( + "evm-metadata-size", + inkwell::module::FlagBehavior::Error, + self.llvm.i64_type().const_int(size, false), + ); + } + let run_init_verify = profiler.start_evm_translation_unit( contract_path, self.code_segment, @@ -264,6 +295,7 @@ impl<'ctx> Context<'ctx> { inkwell::targets::FileType::Assembly, ) .map_err(|error| anyhow::anyhow!("assembly emitting: {error}"))?; + diagnostics.check(is_size_fallback)?; if let Some(output_config) = self.output_config.as_ref() { let assembly_text = String::from_utf8_lossy(assembly_buffer.as_slice()); @@ -311,6 +343,7 @@ impl<'ctx> Context<'ctx> { })?; (bytecode_buffer, None) }; + diagnostics.check(is_size_fallback)?; run_emit_bytecode.borrow_mut().finish(); let immutables = match self.code_segment { @@ -327,8 +360,6 @@ impl<'ctx> Context<'ctx> { let bytecode_size = bytecode_buffer.as_slice().len(); if bytecode_size > bytecode_size_limit { if needs_size_fallback { - crate::codegen::IS_SIZE_FALLBACK - .store(true, std::sync::atomic::Ordering::Relaxed); let mut size_fallback_settings = OptimizerSettings::size(); size_fallback_settings.metadata_size = self.optimizer.settings().metadata_size; self.optimizer = Optimizer::new(size_fallback_settings); diff --git a/solx-codegen-evm/src/codegen/mod.rs b/solx-codegen-evm/src/codegen/mod.rs index a9fef93cf..4660467db 100644 --- a/solx-codegen-evm/src/codegen/mod.rs +++ b/solx-codegen-evm/src/codegen/mod.rs @@ -10,7 +10,6 @@ pub mod profiler; pub mod warning; use std::collections::BTreeMap; -use std::sync::atomic::AtomicBool; use self::context::Context; @@ -37,10 +36,6 @@ pub fn append_metadata( .map_err(|error| anyhow::anyhow!("bytecode metadata appending error: {error}")) } -/// Whether the size fallback is activated during the compilation. -/// Only set once, as we're only compiling one traslation unit in a process. -pub static IS_SIZE_FALLBACK: AtomicBool = AtomicBool::new(false); - /// /// Assembles the main buffer and its dependencies from `bytecode_buffers`. /// diff --git a/solx-codegen-evm/src/diagnostics.rs b/solx-codegen-evm/src/diagnostics.rs new file mode 100644 index 000000000..1b89c8e1c --- /dev/null +++ b/solx-codegen-evm/src/diagnostics.rs @@ -0,0 +1,170 @@ +//! +//! Per-context capture of LLVM diagnostics emitted by the EVM backend. +//! + +use std::cell::Cell; +use std::cell::RefCell; +use std::ffi::c_void; + +use inkwell::context::AsContextRef; +use inkwell::llvm_sys::LLVMDiagnosticSeverity; +use inkwell::llvm_sys::core::LLVMContextSetDiagnosticHandler; +use inkwell::llvm_sys::core::LLVMDisposeMessage; +use inkwell::llvm_sys::core::LLVMGetDiagInfoDescription; +use inkwell::llvm_sys::core::LLVMGetDiagInfoSeverity; +use inkwell::llvm_sys::prelude::LLVMBool; +use inkwell::llvm_sys::prelude::LLVMDiagnosticInfoRef; + +unsafe extern "C" { + /// + /// The EVM-local C API accessor for the stack-region-overflow diagnostic payload. + /// + fn LLVMGetDiagInfoEVMStackRegionOverflow( + info: LLVMDiagnosticInfoRef, + total_stack_size: *mut u64, + stack_region_size: *mut u64, + ) -> LLVMBool; +} + +/// +/// The stack-region-overflow report captured from an EVM backend diagnostic. +/// +/// Surfaced as a typed error so the driver can retry codegen with +/// `total_stack_size` as the new spill area size. +/// +#[derive(Debug, Clone, Copy)] +pub struct StackRegionOverflow { + /// The total stack size the module requires. + pub total_stack_size: u64, + /// The stack region size the module was compiled with. + pub stack_region_size: u64, + /// Whether the overflowing pass was the size fallback. + pub is_size_fallback: bool, +} + +impl std::fmt::Display for StackRegionOverflow { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "total stack size ({}) exceeds the allocated stack region size ({})", + self.total_stack_size, self.stack_region_size + ) + } +} + +impl std::error::Error for StackRegionOverflow {} + +/// +/// The diagnostics recorded by the installed handler. +/// +#[derive(Debug, Default)] +struct Captured { + /// The last stack-region-overflow report. + overflow: Cell>, + /// The first non-overflow error diagnostic. + error: RefCell>, +} + +/// +/// Captures EVM backend diagnostics on an LLVM context. +/// +/// Installing replaces LLVM's default handler, which exits the process on +/// error diagnostics, so `check` must be called after every emission. +/// Dropping uninstalls the handler. +/// +pub(crate) struct Capture<'ctx> { + /// The context the handler is installed on. + llvm: &'ctx inkwell::context::Context, + /// The recorded diagnostics the installed handler writes to. + captured: Box, +} + +impl<'ctx> Capture<'ctx> { + /// + /// Installs the capturing handler on `llvm`. + /// + pub fn install(llvm: &'ctx inkwell::context::Context) -> Self { + let captured = Box::::default(); + unsafe { + LLVMContextSetDiagnosticHandler( + llvm.as_ctx_ref(), + Some(handle), + std::ptr::from_ref::(captured.as_ref()) + .cast_mut() + .cast::(), + ); + } + Self { llvm, captured } + } + + /// + /// Returns the error diagnostic recorded since the last check, if any. + /// + /// A stack-region-overflow report becomes the typed `StackRegionOverflow` + /// error the driver retries on, tagged with `is_size_fallback` so the + /// driver knows which pass overflowed. + /// + pub fn check(&self, is_size_fallback: bool) -> anyhow::Result<()> { + if let Some(mut overflow) = self.captured.overflow.take() { + overflow.is_size_fallback = is_size_fallback; + return Err(anyhow::Error::new(overflow)); + } + if let Some(error) = self.captured.error.borrow_mut().take() { + anyhow::bail!("LLVM diagnostic: {error}"); + } + Ok(()) + } +} + +impl Drop for Capture<'_> { + fn drop(&mut self) { + unsafe { + LLVMContextSetDiagnosticHandler(self.llvm.as_ctx_ref(), None, std::ptr::null_mut()); + } + } +} + +/// +/// Records stack-region-overflow reports and other error diagnostics, and +/// forwards the rest to `stderr` in place of LLVM's default handler. +/// +extern "C" fn handle(info: LLVMDiagnosticInfoRef, captured: *mut c_void) { + let captured = unsafe { &*captured.cast_const().cast::() }; + + let mut total_stack_size = 0u64; + let mut stack_region_size = 0u64; + let is_overflow = unsafe { + LLVMGetDiagInfoEVMStackRegionOverflow(info, &mut total_stack_size, &mut stack_region_size) + } != 0; + if is_overflow { + captured.overflow.set(Some(StackRegionOverflow { + total_stack_size, + stack_region_size, + is_size_fallback: false, + })); + return; + } + + let severity = unsafe { LLVMGetDiagInfoSeverity(info) }; + if !matches!( + severity, + LLVMDiagnosticSeverity::LLVMDSError | LLVMDiagnosticSeverity::LLVMDSWarning + ) { + return; + } + + let description = unsafe { LLVMGetDiagInfoDescription(info) }; + let message = unsafe { std::ffi::CStr::from_ptr(description) } + .to_string_lossy() + .into_owned(); + unsafe { LLVMDisposeMessage(description) }; + + if let LLVMDiagnosticSeverity::LLVMDSError = severity { + let mut error = captured.error.borrow_mut(); + if error.is_none() { + *error = Some(message); + } + return; + } + eprintln!("LLVM warning: {message}"); +} diff --git a/solx-codegen-evm/src/lib.rs b/solx-codegen-evm/src/lib.rs index 17386f6f0..1d9d10a7e 100644 --- a/solx-codegen-evm/src/lib.rs +++ b/solx-codegen-evm/src/lib.rs @@ -10,11 +10,11 @@ pub(crate) mod r#const; pub(crate) mod context; pub(crate) mod debug_config; pub(crate) mod dependencies; +pub(crate) mod diagnostics; pub(crate) mod optimizer; pub(crate) mod target_machine; pub use self::codegen::DummyLLVMWritable; -pub use self::codegen::IS_SIZE_FALLBACK; pub use self::codegen::WriteLLVM; pub use self::codegen::append_metadata; pub use self::codegen::assemble; @@ -71,6 +71,7 @@ pub use self::debug_config::DebugConfig; pub use self::debug_config::OutputConfig; pub use self::debug_config::ir_type::IRType; pub use self::dependencies::Dependencies; +pub use self::diagnostics::StackRegionOverflow; pub use self::optimizer::Optimizer; pub use self::optimizer::settings::Settings as OptimizerSettings; pub use self::optimizer::settings::size_level::SizeLevel; diff --git a/solx-codegen-evm/src/target_machine.rs b/solx-codegen-evm/src/target_machine.rs index 26fd2464d..62e887b90 100644 --- a/solx-codegen-evm/src/target_machine.rs +++ b/solx-codegen-evm/src/target_machine.rs @@ -22,34 +22,21 @@ impl TargetMachine { /// /// A shortcut constructor. /// - /// Supported LLVM options: - /// `-evm-stack-region-size ` - /// `-evm-stack-region-offset ` - /// `-evm-metadata-size ` - /// - /// LLVM command line options are process-global, so their occurrences are reset before - /// each parse: a unit never inherits an option set by a previous one in the same worker. + /// Per-unit codegen parameters (stack region, metadata size) travel as + /// module flags set in `Context::build`, not as LLVM options: options are + /// process-global, module flags are per-module. /// pub fn new( optimizer_settings: &OptimizerSettings, llvm_options: &[String], ) -> anyhow::Result { - let mut arguments = Vec::with_capacity(4 + llvm_options.len()); + let mut arguments = Vec::with_capacity(1 + llvm_options.len()); arguments.push(Self::TARGET.to_string()); arguments.extend_from_slice(llvm_options); - if let Some(size) = optimizer_settings.spill_area_size { - arguments.push(format!( - "-evm-stack-region-offset={}", - crate::r#const::SOLC_USER_MEMORY_OFFSET - )); - arguments.push(format!("-evm-stack-region-size={size}")); - } - if let Some(size) = optimizer_settings.metadata_size { - arguments.push(format!("-evm-metadata-size={size}")); + if arguments.len() > 1 { + let arguments: Vec<&str> = arguments.iter().map(|argument| argument.as_str()).collect(); + inkwell::support::parse_command_line_options(arguments.as_slice(), "LLVM options"); } - let arguments: Vec<&str> = arguments.iter().map(|argument| argument.as_str()).collect(); - inkwell::support::reset_all_option_occurrences(); - inkwell::support::parse_command_line_options(arguments.as_slice(), "LLVM options"); let target_machine = inkwell::targets::Target::from_name(Self::TARGET.to_string().as_str()) .ok_or_else(|| anyhow::anyhow!("LLVM target machine `{}` not found", Self::TARGET))? diff --git a/solx-core/src/error/mod.rs b/solx-core/src/error/mod.rs index 2bf7b5dc0..b6f0d0517 100644 --- a/solx-core/src/error/mod.rs +++ b/solx-core/src/error/mod.rs @@ -51,7 +51,12 @@ impl Error { impl From for Error { fn from(error: anyhow::Error) -> Self { - Error::Generic(error.to_string()) + match error.downcast::() { + Ok(overflow) => { + Error::stack_too_deep(overflow.total_stack_size, overflow.is_size_fallback) + } + Err(error) => Error::Generic(error.to_string()), + } } } diff --git a/solx-core/src/process/child.rs b/solx-core/src/process/child.rs index 07dfc2ed1..4daea592c 100644 --- a/solx-core/src/process/child.rs +++ b/solx-core/src/process/child.rs @@ -2,7 +2,6 @@ //! The subprocess-side worker: reads a session, then compiles jobs until `stdin` closes. //! -use std::sync::atomic::Ordering; use std::thread::Builder; use crate::error::Error; @@ -25,10 +24,7 @@ pub fn run() -> anyhow::Result<()> { .recv()? .ok_or_else(|| anyhow::anyhow!("The worker received no session"))?; - inkwell::support::error_handling::install_stack_error_handler(evm_stack_error_handler); - while let Some(job) = stdin.recv::()? { - solx_codegen_evm::IS_SIZE_FALLBACK.store(false, Ordering::Relaxed); let result = Contract::compile_to_evm( session.language, session.solc_version.clone(), @@ -63,23 +59,3 @@ pub fn run() -> anyhow::Result<()> { .join() .expect("Threading error") } - -/// -/// Handles LLVM stack-too-deep errors. -/// -/// # Safety -/// -/// This function is unsafe because it is called from the LLVM stackifier. -/// The function must terminate the process after handling the error. -/// -unsafe extern "C" fn evm_stack_error_handler(spill_area_size: u64) { - let result: crate::Result = Err(Error::stack_too_deep( - spill_area_size, - solx_codegen_evm::IS_SIZE_FALLBACK.load(Ordering::Relaxed), - )); - std::io::stdout() - .send(&result) - .unwrap_or_else(|error| panic!("Stack-too-deep response writing error: {error}")); - unsafe { inkwell::support::shutdown_llvm() }; - std::process::exit(solx_utils::EXIT_CODE_SUCCESS); -} diff --git a/solx-core/src/process/pool.rs b/solx-core/src/process/pool.rs index 7383d950d..4bebd4e2f 100644 --- a/solx-core/src/process/pool.rs +++ b/solx-core/src/process/pool.rs @@ -4,16 +4,21 @@ use std::path::PathBuf; use std::sync::Mutex; +use std::sync::Once; use crate::error::Error; use crate::process::job::Job; use crate::process::output::Output as EVMOutput; use crate::process::session::Session; use crate::process::worker::Worker; +use crate::project::contract::Contract; /// The lock-poisoning invariant shared by the idle-pool accessors. const POISON: &str = "lock is never poisoned because worker threads do not panic"; +/// One-time installation of the in-process fatal error handler. +static FATAL_ERROR_HANDLER: Once = Once::new(); + /// /// The pool of persistent worker subprocesses. /// @@ -22,6 +27,11 @@ const POISON: &str = "lock is never poisoned because worker threads do not panic /// after which the child exits. The number of live workers never exceeds the number of /// dispatching threads. /// +/// With `SOLX_IN_PROCESS` set, jobs compile on the dispatching threads themselves and no +/// subprocess is ever spawned. Codegen state is per-`LLVMContext` and per-module, so +/// concurrent in-process jobs do not interfere; the trade-off is isolation — a crash or +/// LLVM fatal error takes down the whole compiler, not one worker. +/// pub struct Pool { /// The worker executable path. executable: PathBuf, @@ -29,6 +39,8 @@ pub struct Pool { session: Session, /// The idle workers available for checkout. idle: Mutex>, + /// Whether jobs compile on the dispatching threads instead of worker subprocesses. + in_process: bool, } impl Pool { @@ -36,6 +48,12 @@ impl Pool { /// Creates a pool that dispatches jobs of `session` to worker subprocesses. /// pub fn new(session: Session) -> anyhow::Result { + let in_process = std::env::var_os("SOLX_IN_PROCESS").is_some_and(|value| value != "0"); + if in_process { + FATAL_ERROR_HANDLER.call_once(|| unsafe { + inkwell::support::error_handling::install_fatal_error_handler(fatal_error_handler); + }); + } let executable = crate::process::EXECUTABLE .get() .cloned() @@ -46,6 +64,7 @@ impl Pool { executable, session, idle: Mutex::new(Vec::new()), + in_process, }) } @@ -56,6 +75,9 @@ impl Pool { /// A transport failure or a `StackTooDeep` reply retires it instead. /// pub fn execute(&self, job: &Job) -> crate::Result { + if self.in_process { + return self.execute_in_process(job); + } let mut worker = match self.idle.lock().expect(POISON).pop() { Some(worker) => worker, None => Worker::spawn(self.executable.as_path(), &self.session)?, @@ -73,4 +95,55 @@ impl Pool { } } } + + /// + /// Compiles one translation unit on the calling thread. + /// + /// The job takes the same serde roundtrip a worker subprocess would receive, so both + /// modes compile identical inputs. + /// + fn execute_in_process(&self, job: &Job) -> crate::Result { + let mut buffer = Vec::with_capacity(crate::r#const::DEFAULT_SERDE_BUFFER_SIZE); + ciborium::into_writer(job, &mut buffer) + .map_err(|error| anyhow::anyhow!("In-process job serializing error: {error}"))?; + let job: Job = + ciborium::de::from_reader_with_recursion_limit(buffer.as_slice(), usize::MAX) + .map_err(|error| anyhow::anyhow!("In-process job deserializing error: {error}"))?; + + let contract_path = job.contract_name.path.clone(); + Contract::compile_to_evm( + self.session.language, + self.session.solc_version.clone(), + job.contract_name, + job.contract_ir, + job.code_segment, + self.session.evm_version, + job.debug_info, + &self.session.output_selection, + job.immutables, + job.metadata_bytes, + job.optimizer_settings, + self.session.llvm_options.clone(), + self.session.output_config.clone(), + ) + .map(EVMOutput::new) + .map_err(|error| match error { + Error::Generic(error) => solx_standard_json::OutputError::new_error_contract( + Some(contract_path.as_str()), + error, + ) + .into(), + error => error, + }) + } +} + +/// +/// Aborts on LLVM fatal errors: the default handler exits via `exit(1)`, whose atexit +/// destructors tear down LLVM globals under concurrently compiling threads. +/// +extern "C" fn fatal_error_handler(message: *const std::ffi::c_char) { + let message = unsafe { std::ffi::CStr::from_ptr(message) }.to_string_lossy(); + eprintln!("LLVM fatal error: {message}"); + std::process::abort(); }