Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions solx-codegen-evm/src/codegen/context/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,37 @@ impl<'ctx> Context<'ctx> {
) -> anyhow::Result<EVMBuild> {
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,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down
5 changes: 0 additions & 5 deletions solx-codegen-evm/src/codegen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ pub mod profiler;
pub mod warning;

use std::collections::BTreeMap;
use std::sync::atomic::AtomicBool;

use self::context::Context;

Expand All @@ -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`.
///
Expand Down
170 changes: 170 additions & 0 deletions solx-codegen-evm/src/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -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<Option<StackRegionOverflow>>,
/// The first non-overflow error diagnostic.
error: RefCell<Option<String>>,
}

///
/// 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<Captured>,
}

impl<'ctx> Capture<'ctx> {
///
/// Installs the capturing handler on `llvm`.
///
pub fn install(llvm: &'ctx inkwell::context::Context) -> Self {
let captured = Box::<Captured>::default();
unsafe {
LLVMContextSetDiagnosticHandler(
llvm.as_ctx_ref(),
Some(handle),
std::ptr::from_ref::<Captured>(captured.as_ref())
.cast_mut()
.cast::<c_void>(),
);
}
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::<Captured>() };

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);
}
Comment on lines +162 to +166
return;
}
eprintln!("LLVM warning: {message}");
}
3 changes: 2 additions & 1 deletion solx-codegen-evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
27 changes: 7 additions & 20 deletions solx-codegen-evm/src/target_machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,34 +22,21 @@ impl TargetMachine {
///
/// A shortcut constructor.
///
/// Supported LLVM options:
/// `-evm-stack-region-size <value>`
/// `-evm-stack-region-offset <value>`
/// `-evm-metadata-size <value>`
///
/// 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<Self> {
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");
}
Comment on lines +33 to 39
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))?
Expand Down
7 changes: 6 additions & 1 deletion solx-core/src/error/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,12 @@ impl Error {

impl From<anyhow::Error> for Error {
fn from(error: anyhow::Error) -> Self {
Error::Generic(error.to_string())
match error.downcast::<solx_codegen_evm::StackRegionOverflow>() {
Ok(overflow) => {
Error::stack_too_deep(overflow.total_stack_size, overflow.is_size_fallback)
}
Err(error) => Error::Generic(error.to_string()),
}
}
}

Expand Down
24 changes: 0 additions & 24 deletions solx-core/src/process/child.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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::<Job>()? {
solx_codegen_evm::IS_SIZE_FALLBACK.store(false, Ordering::Relaxed);
let result = Contract::compile_to_evm(
session.language,
session.solc_version.clone(),
Expand Down Expand Up @@ -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<EVMOutput> = 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);
}
Loading
Loading