diff --git a/.github/workflows/integration-tests.yaml b/.github/workflows/integration-tests.yaml index 0ad844483..8974b5daf 100644 --- a/.github/workflows/integration-tests.yaml +++ b/.github/workflows/integration-tests.yaml @@ -48,10 +48,16 @@ jobs: contents: read pull-requests: write packages: read - runs-on: solx-linux-amd64-self-hosted - container: - image: ghcr.io/nomicfoundation/solx-ci-runner@sha256:a3e9312d6442e028a4b9eed17ea597380ae86abf3ff4d45ba957d5a10a805790 - options: -m 110g + # HACK (temporary, for testing PR #524 on large macOS runners): run the + # integration suite on GitHub's large macOS runners instead of the + # self-hosted Linux container. macOS runners can't use a job `container:`, + # so it is dropped and LLVM/solc are built from source on the runner (same + # build-llvm/build-solc actions used by test.yaml's macOS legs). + strategy: + fail-fast: false + matrix: + runner: [macos-15-large, macos-15-xlarge] + runs-on: ${{ matrix.runner }} steps: - name: Checkout PR uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -84,6 +90,83 @@ jobs: with: working-directory: temp-solx-main + # LLVM is built from source here (up to twice: PR + main baseline) and + # will exhaust the runner disk without this. Copied verbatim from + # test.yaml's macOS legs. + - name: Free disk space (macOS) + if: runner.os == 'macOS' + shell: bash + run: | + set -euo pipefail + echo "=== Before macOS cleanup ===" && df -h . + + # 1. Simulator runtimes — stored on read-only APFS snapshot volumes, + # so plain `rm` fails. `simctl runtime delete all` unmounts them. + # May warn on already-deleted runtimes; that is harmless. + echo "--- Removing simulator runtimes ---" + xcrun simctl delete all 2>&1 || true + xcrun simctl runtime delete all 2>&1 || true + + # 2. Xcode — remove every versioned copy EXCEPT the one that + # xcode-select points to (we need its toolchain for C/C++ builds). + # Active Xcode path looks like /Applications/Xcode_16.2.app/Contents/Developer. + echo "--- Removing inactive Xcode versions ---" + ACTIVE_XCODE="$(xcode-select -p 2>/dev/null | sed 's|/Contents/Developer/*$||' || true)" + ACTIVE_XCODE="${ACTIVE_XCODE%/}" + removed=0 + if [ -z "${ACTIVE_XCODE}" ] || [[ "${ACTIVE_XCODE}" != /Applications/Xcode*.app ]]; then + echo " warning: active Xcode path '${ACTIVE_XCODE}' is not an Xcode app; skipping Xcode removal" + else + echo "Active Xcode (keeping): ${ACTIVE_XCODE}" + to_remove=() + for app in /Applications/Xcode_*.app; do + [ -d "$app" ] || continue + if [ "$app" = "$ACTIVE_XCODE" ]; then + echo " skip (active): $app" + else + echo " removing: $app" + to_remove+=("$app") + fi + done + # Each Xcode bundle is ~15 GB of small files; `rm -rf` is I/O-bound + # per inode, so run them concurrently (one worker per bundle). + # Soft-fail: a stray rm error shouldn't sink the whole job — rm's + # stderr will pinpoint the bad path above. + # + # `removed` reflects the attempt count, not per-bundle success: + # xargs returns non-zero if *any* child failed, so gating the + # count on xargs success would print "Removed 0" even after + # ~30 GB was freed. The count is off by the number of failed + # bundles — usually one in practice — which is still a much + # better signal than zero. + if [ "${#to_remove[@]}" -gt 0 ]; then + removed=${#to_remove[@]} + if ! printf '%s\0' "${to_remove[@]}" \ + | xargs -0 -n1 -P "${#to_remove[@]}" sudo rm -rf; then + echo " warning: one or more Xcode removals failed (see rm stderr above)" + fi + fi + fi + echo "Removed ${removed} inactive Xcode version(s)" + + # 3. Remaining large packages that this project never uses. + # Each path is removed individually so a missing path doesn't + # mask a real permission error on another. + echo "--- Removing unused SDKs and caches ---" + for dir in \ + /Library/Developer/CoreSimulator \ + /usr/local/lib/android \ + "${RUNNER_TOOL_CACHE:-/Users/runner/hostedtoolcache}"; do + if [ -d "$dir" ]; then + echo " removing: $dir" + sudo rm -rf "$dir" || echo " warning: failed to remove $dir" + else + echo " not found (skipped): $dir" + fi + done + + echo "=== After macOS cleanup ===" && df -h . + - name: Setup SFW uses: ./.github/actions/setup-sfw @@ -281,16 +364,16 @@ jobs: id: solx-tester-report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: solx-tester-report + name: solx-tester-report-${{ matrix.runner }} path: solx-tester-report.xlsx - name: Post solx-tester report comment if: always() && steps.solx-tester-report.outcome == 'success' uses: mshick/add-pr-comment@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12 with: - message-id: 'solx-tester-report' + message-id: 'solx-tester-report-${{ matrix.runner }}' message: | - 📊 **solx Tester Report** + 📊 **solx Tester Report** (`${{ matrix.runner }}`) ➡️ [**Download**](${{ steps.solx-tester-report.outputs.artifact-url }}) @@ -323,16 +406,16 @@ jobs: id: hardhat-report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: hardhat-report + name: hardhat-report-${{ matrix.runner }} path: ./temp-hardhat-reports/hardhat-report.xlsx - name: Post Hardhat report comment if: always() && steps.hardhat-report.outcome == 'success' uses: mshick/add-pr-comment@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12 with: - message-id: 'hardhat-report' + message-id: 'hardhat-report-${{ matrix.runner }}' message: | - 📊 **Hardhat Projects Report** + 📊 **Hardhat Projects Report** (`${{ matrix.runner }}`) ➡️ [**Download**](${{ steps.hardhat-report.outputs.artifact-url }}) @@ -346,16 +429,16 @@ jobs: id: foundry-report uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: - name: foundry-report + name: foundry-report-${{ matrix.runner }} path: ./temp-foundry-reports/foundry-report.xlsx - name: Post Foundry report comment if: always() && steps.foundry-report.outcome == 'success' uses: mshick/add-pr-comment@ec328af66588ab8f77cdeb2c264f14aba45bbf59 # v3.12 with: - message-id: 'foundry-report' + message-id: 'foundry-report-${{ matrix.runner }}' message: | - 📊 **Foundry Projects Report** + 📊 **Foundry Projects Report** (`${{ matrix.runner }}`) ➡️ [**Download**](${{ steps.foundry-report.outputs.artifact-url }}) diff --git a/Cargo.lock b/Cargo.lock index 804e3a475..49d77658d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2341,7 +2341,7 @@ dependencies = [ [[package]] name = "inkwell" version = "0.7.1" -source = "git+https://github.com/NomicFoundation/inkwell?rev=4f9f86e15f43dc8555b8b51d4dda01e60f756f22#4f9f86e15f43dc8555b8b51d4dda01e60f756f22" +source = "git+https://github.com/NomicFoundation/inkwell?branch=az-reset-option-occurrences#dd7c761865dc985a07a6c63f6de2614388d49cfd" dependencies = [ "inkwell_internals", "libc", @@ -2354,7 +2354,7 @@ dependencies = [ [[package]] name = "inkwell_internals" version = "0.12.0" -source = "git+https://github.com/NomicFoundation/inkwell?rev=4f9f86e15f43dc8555b8b51d4dda01e60f756f22#4f9f86e15f43dc8555b8b51d4dda01e60f756f22" +source = "git+https://github.com/NomicFoundation/inkwell?branch=az-reset-option-occurrences#dd7c761865dc985a07a6c63f6de2614388d49cfd" dependencies = [ "proc-macro2", "quote", @@ -2602,7 +2602,7 @@ checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" [[package]] name = "llvm-sys" version = "211.0.0" -source = "git+https://github.com/NomicFoundation/llvm-sys.rs?branch=main#afdfa3146ca4dcc4f9bf023fb5caa3fe14f5d908" +source = "git+https://github.com/NomicFoundation/llvm-sys.rs?branch=az-reset-option-occurrences#bfc06932fc84da16a36017a77a1b7f940813adf0" dependencies = [ "anyhow", "cc", diff --git a/Cargo.toml b/Cargo.toml index 763a25ef3..03ef39467 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,7 +87,7 @@ features = ["http-rustls-tls", "test", "signing"] # LLVM [workspace.dependencies.inkwell] git = "https://github.com/NomicFoundation/inkwell" -rev = "4f9f86e15f43dc8555b8b51d4dda01e60f756f22" +branch = "az-reset-option-occurrences" default-features = false features = [ "llvm21-1", diff --git a/solx-codegen-evm/src/codegen/context/mod.rs b/solx-codegen-evm/src/codegen/context/mod.rs index 9134da0a2..82c9879a1 100644 --- a/solx-codegen-evm/src/codegen/context/mod.rs +++ b/solx-codegen-evm/src/codegen/context/mod.rs @@ -323,14 +323,10 @@ impl<'ctx> Context<'ctx> { && self.optimizer.settings().is_fallback_to_size_enabled() { crate::codegen::IS_SIZE_FALLBACK - .compare_exchange( - false, - true, - std::sync::atomic::Ordering::Relaxed, - std::sync::atomic::Ordering::Relaxed, - ) - .expect("Failed to set the global size fallback flag"); - self.optimizer = Optimizer::new(OptimizerSettings::size()); + .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); self.module = module_size_fallback; for function in self.module.get_functions() { Function::set_size_attributes(self.llvm, function); diff --git a/solx-codegen-evm/src/context/traits/evmla_stack.rs b/solx-codegen-evm/src/context/traits/evmla_stack.rs index 795032430..1a75885ba 100644 --- a/solx-codegen-evm/src/context/traits/evmla_stack.rs +++ b/solx-codegen-evm/src/context/traits/evmla_stack.rs @@ -46,13 +46,12 @@ pub trait IEVMLAStack<'ctx>: IContext<'ctx> + Sized { for position in 0..depth { if let ShadowSlot::Memory(index) = self.evmla().expect("Always exists").shadow_peek(position) + && index != position { - if index != position { - let value = self.evmla_stack_read(position)?; - self.evmla_mut() - .expect("Always exists") - .shadow_write(position, value); - } + let value = self.evmla_stack_read(position)?; + self.evmla_mut() + .expect("Always exists") + .shadow_write(position, value); } } for position in 0..depth { diff --git a/solx-codegen-evm/src/target_machine.rs b/solx-codegen-evm/src/target_machine.rs index 5aea7e2d5..26fd2464d 100644 --- a/solx-codegen-evm/src/target_machine.rs +++ b/solx-codegen-evm/src/target_machine.rs @@ -27,11 +27,14 @@ impl TargetMachine { /// `-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. + /// pub fn new( optimizer_settings: &OptimizerSettings, llvm_options: &[String], ) -> anyhow::Result { - let mut arguments = Vec::with_capacity(1 + llvm_options.len()); + let mut arguments = Vec::with_capacity(4 + llvm_options.len()); arguments.push(Self::TARGET.to_string()); arguments.extend_from_slice(llvm_options); if let Some(size) = optimizer_settings.spill_area_size { @@ -44,10 +47,9 @@ impl TargetMachine { 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/arguments.rs b/solx-core/src/arguments.rs index 9f69845ff..964efb718 100644 --- a/solx-core/src/arguments.rs +++ b/solx-core/src/arguments.rs @@ -244,15 +244,15 @@ pub struct Arguments { #[arg(long, help_heading = "Debug Options")] pub llvm_debug_logging: bool, - /// Run this process recursively and provide JSON input to compile a single contract. + /// Run this process as a persistent worker compiling contracts fed via `stdin`. /// Only for usage from within the compiler. #[arg(long, hide = true)] pub recursive_process: bool, } impl Arguments { - /// Expected argument count for `--recursive-process` (binary name + flag + value). - const RECURSIVE_PROCESS_MAX_ARGS: usize = 3; + /// Expected argument count for `--recursive-process` (binary name + flag). + const RECURSIVE_PROCESS_MAX_ARGS: usize = 2; /// Expected argument count for `--version` (binary name + flag). const VERSION_MAX_ARGS: usize = 2; diff --git a/solx-core/src/compiler.rs b/solx-core/src/compiler.rs index 84f710b74..f3d9e9a0e 100644 --- a/solx-core/src/compiler.rs +++ b/solx-core/src/compiler.rs @@ -29,14 +29,25 @@ impl<'arguments> Compiler<'arguments> { } /// - /// Initialize the compiler runtime: rayon thread pool, LLVM stack trace, and - /// EVM target. + /// Initialize the compiler runtime: LLVM stack trace, EVM target, and + /// rayon thread pool. /// - /// If `arguments.recursive_process` is set, runs the subprocess handler and + /// If `arguments.recursive_process` is set, runs the worker subprocess loop and /// returns `Ok(true)` -- the caller should return immediately. /// Otherwise returns `Ok(false)`. /// + /// The rayon thread pool is built after the worker branch: workers compile + /// one translation unit at a time and never use it. + /// pub fn initialize(&self) -> anyhow::Result { + inkwell::support::enable_llvm_pretty_stack_trace(); + solx_codegen_evm::initialize_target(); + + if self.arguments.recursive_process { + crate::run_subprocess()?; + return Ok(true); + } + let mut thread_pool_builder = rayon::ThreadPoolBuilder::new(); if let Some(threads) = self.arguments.threads { thread_pool_builder = thread_pool_builder.num_threads(threads); @@ -46,14 +57,6 @@ impl<'arguments> Compiler<'arguments> { .build_global() .expect("rayon thread pool parameters are valid"); - inkwell::support::enable_llvm_pretty_stack_trace(); - solx_codegen_evm::initialize_target(); - - if self.arguments.recursive_process { - crate::run_subprocess()?; - return Ok(true); - } - Ok(false) } diff --git a/solx-core/src/lib.rs b/solx-core/src/lib.rs index 566dfdfb9..2d9de9880 100644 --- a/solx-core/src/lib.rs +++ b/solx-core/src/lib.rs @@ -28,9 +28,11 @@ pub use self::error::Error; pub use self::error::stack_too_deep::StackTooDeep as StackTooDeepError; pub use self::frontend::Frontend; pub use self::process::EXECUTABLE; -pub use self::process::input::Input as EVMProcessInput; +pub use self::process::child::run as run_subprocess; +pub use self::process::job::Job as EVMProcessJob; pub use self::process::output::Output as EVMProcessOutput; -pub use self::process::run as run_subprocess; +pub use self::process::pool::Pool as EVMProcessPool; +pub use self::process::session::Session as EVMProcessSession; pub use self::project::Project; pub use self::project::contract::Contract as ProjectContract; diff --git a/solx-core/src/process/channel.rs b/solx-core/src/process/channel.rs new file mode 100644 index 000000000..0a9d9c7de --- /dev/null +++ b/solx-core/src/process/channel.rs @@ -0,0 +1,68 @@ +//! +//! The length-prefixed CBOR frame protocol shared by both sides of the worker channel. +//! + +use std::io::BufRead; +use std::io::Write; + +/// The size of the frame length prefix. +const LENGTH_PREFIX_SIZE: usize = size_of::(); + +/// +/// Writes length-prefixed CBOR frames. +/// +pub trait FrameWrite: Write { + /// + /// Serializes `value` into a length-prefixed CBOR frame and flushes it. + /// + fn send(&mut self, value: &T) -> anyhow::Result<()> + where + T: serde::Serialize, + { + let mut frame = vec![0u8; LENGTH_PREFIX_SIZE]; + ciborium::into_writer(value, &mut frame) + .map_err(|error| anyhow::anyhow!("Frame serializing error: {error}"))?; + let body_length = (frame.len() - LENGTH_PREFIX_SIZE).to_le_bytes(); + frame[..LENGTH_PREFIX_SIZE].copy_from_slice(body_length.as_slice()); + self.write_all(frame.as_slice()) + .and_then(|()| self.flush()) + .map_err(|error| anyhow::anyhow!("Frame writing error: {error}")) + } +} + +impl FrameWrite for W {} + +/// +/// Reads length-prefixed CBOR frames. +/// +pub trait FrameRead: BufRead { + /// + /// Reads one length-prefixed CBOR frame, or `None` when the stream is at a frame boundary EOF. + /// + /// An empty buffer means the stream ended cleanly between frames; once a frame has + /// started, `read_exact` turns any short read into a truncation error. + /// + fn recv(&mut self) -> anyhow::Result> + where + T: serde::de::DeserializeOwned, + { + if self + .fill_buf() + .map_err(|error| anyhow::anyhow!("Frame reading error: {error}"))? + .is_empty() + { + return Ok(None); + } + let mut length_bytes = [0u8; LENGTH_PREFIX_SIZE]; + self.read_exact(length_bytes.as_mut_slice()) + .map_err(|error| anyhow::anyhow!("Frame length prefix reading error: {error}"))?; + let mut body = vec![0u8; usize::from_le_bytes(length_bytes)]; + self.read_exact(body.as_mut_slice()) + .map_err(|error| anyhow::anyhow!("Frame body reading error: {error}"))?; + ciborium::de::from_reader_with_recursion_limit(body.as_slice(), usize::MAX) + .map(Some) + .map_err(|error| anyhow::anyhow!("Frame deserializing error: {error}")) + } +} + +impl FrameRead for R {} diff --git a/solx-core/src/process/child.rs b/solx-core/src/process/child.rs new file mode 100644 index 000000000..07dfc2ed1 --- /dev/null +++ b/solx-core/src/process/child.rs @@ -0,0 +1,85 @@ +//! +//! 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; +use crate::process::channel::FrameRead; +use crate::process::channel::FrameWrite; +use crate::process::job::Job; +use crate::process::output::Output as EVMOutput; +use crate::process::session::Session; +use crate::project::contract::Contract; + +/// +/// Runs the worker loop on a dedicated stack-sized thread until `stdin` closes. +/// +pub fn run() -> anyhow::Result<()> { + Builder::new() + .stack_size(crate::WORKER_THREAD_STACK_SIZE) + .spawn(|| -> anyhow::Result<()> { + let mut stdin = std::io::stdin().lock(); + let session: Session = stdin + .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(), + job.contract_name.clone(), + job.contract_ir, + job.code_segment, + session.evm_version, + job.debug_info, + &session.output_selection, + job.immutables, + job.metadata_bytes, + job.optimizer_settings, + session.llvm_options.clone(), + session.output_config.clone(), + ) + .map(EVMOutput::new) + .map_err(|error| match error { + Error::Generic(error) => solx_standard_json::OutputError::new_error_contract( + Some(job.contract_name.path.as_str()), + error, + ) + .into(), + error => error, + }); + std::io::stdout().send(&result)?; + } + + unsafe { inkwell::support::shutdown_llvm() }; + Ok(()) + }) + .expect("Threading error") + .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/input.rs b/solx-core/src/process/job.rs similarity index 53% rename from solx-core/src/process/input.rs rename to solx-core/src/process/job.rs index c5808d41c..b86715ff4 100644 --- a/solx-core/src/process/input.rs +++ b/solx-core/src/process/job.rs @@ -1,7 +1,5 @@ //! -//! Process for compiling a single compilation unit. -//! -//! The EVM input data. +//! The per-unit job data. //! use std::collections::BTreeMap; @@ -10,71 +8,49 @@ use std::collections::BTreeSet; use crate::project::contract::ir::IR as ContractIR; /// -/// The EVM input data. +/// The per-unit job data. +/// +/// Sent to a worker subprocess for every translation unit, complementing the session data. /// #[derive(Debug, serde::Serialize, serde::Deserialize)] -pub struct Input { - /// The input contract language. - pub language: solx_standard_json::InputLanguage, - /// The `solc` compiler version, used only for Solidity and Yul projects. - pub solc_version: Option, +pub struct Job { /// The input contract name. pub contract_name: solx_utils::ContractName, /// The input contract IR. pub contract_ir: ContractIR, /// The code segment. pub code_segment: solx_utils::CodeSegment, - /// The EVM version to produce bytecode for. - pub evm_version: Option, /// Solidity debug info. pub debug_info: Option, - /// Output selection for the compilation. - pub output_selection: solx_standard_json::InputSelection, /// Immutables produced by the runtime code run. pub immutables: Option>>, /// The metadata bytes. pub metadata_bytes: Option>, /// The optimizer settings. pub optimizer_settings: solx_codegen_evm::OptimizerSettings, - /// The extra LLVM arguments. - pub llvm_options: Vec, - /// The output config for IR artifacts. - pub output_config: Option, } -impl Input { +impl Job { /// /// A shortcut constructor. /// pub fn new( - language: solx_standard_json::InputLanguage, - solc_version: Option, contract_name: solx_utils::ContractName, contract_ir: ContractIR, code_segment: solx_utils::CodeSegment, - evm_version: Option, debug_info: Option, - output_selection: solx_standard_json::InputSelection, immutables: Option>>, metadata_bytes: Option>, optimizer_settings: solx_codegen_evm::OptimizerSettings, - llvm_options: Vec, - output_config: Option, ) -> Self { Self { - language, - solc_version, contract_name, contract_ir, code_segment, - evm_version, debug_info, - output_selection, immutables, metadata_bytes, optimizer_settings, - llvm_options, - output_config, } } } diff --git a/solx-core/src/process/mod.rs b/solx-core/src/process/mod.rs index ed42e7ede..96f36d43b 100644 --- a/solx-core/src/process/mod.rs +++ b/solx-core/src/process/mod.rs @@ -1,182 +1,17 @@ //! -//! Process for compiling a single compilation unit. +//! The persistent worker subprocess pool and its framed session/job protocol. //! -pub mod input; +pub mod channel; +pub mod child; +pub mod job; pub mod output; +pub mod pool; +pub mod session; +pub mod worker; -use std::io::Read; -use std::io::Write; use std::path::PathBuf; -use std::process::Command; use std::sync::OnceLock; -use std::thread::Builder; - -use crate::error::Error; -use crate::project::contract::Contract; - -use self::input::Input as EVMInput; -use self::output::Output as EVMOutput; /// The overridden executable name used when the compiler is run as a library. pub static EXECUTABLE: OnceLock = OnceLock::new(); - -/// -/// Read input from `stdin`, compile a contract, and write the output to `stdout`. -/// -pub fn run() -> anyhow::Result<()> { - let length_bytes = { - let mut buffer = [0u8; 8]; - std::io::stdin() - .read_exact(&mut buffer) - .map_err(|error| anyhow::anyhow!("Input length prefix reading error: {error}"))?; - usize::from_le_bytes(buffer) - }; - let mut buffer = Vec::with_capacity(length_bytes); - std::io::stdin() - .read_to_end(&mut buffer) - .map_err(|error| anyhow::anyhow!("Input reading error: {error}"))?; - let input: EVMInput = - ciborium::de::from_reader_with_recursion_limit(buffer.as_slice(), usize::MAX) - .map_err(|error| anyhow::anyhow!("Input deserialziing error: {error}"))?; - - let result = Builder::new() - .stack_size(crate::WORKER_THREAD_STACK_SIZE) - .spawn(move || { - Contract::compile_to_evm( - input.language, - input.solc_version, - input.contract_name.clone(), - input.contract_ir, - input.code_segment, - input.evm_version, - input.debug_info, - input.output_selection, - input.immutables, - input.metadata_bytes, - input.optimizer_settings, - input.llvm_options, - input.output_config, - ) - .map(EVMOutput::new) - .map_err(|error| match error { - Error::Generic(error) => solx_standard_json::OutputError::new_error_contract( - Some(input.contract_name.path.as_str()), - error, - ) - .into(), - error => error, - }) - }) - .expect("Threading error") - .join() - .expect("Threading error"); - - ciborium::into_writer(&result, &mut std::io::stdout()) - .map_err(|error| anyhow::anyhow!("Result serializing and writing error: {error}"))?; - unsafe { inkwell::support::shutdown_llvm() }; - Ok(()) -} - -/// -/// Runs this process recursively to compile a single contract. -/// -pub fn call(contract_name: &solx_utils::ContractName, input: &I) -> crate::Result -where - I: serde::Serialize, - O: serde::de::DeserializeOwned, -{ - let executable = EXECUTABLE - .get() - .cloned() - .unwrap_or_else(|| std::env::current_exe().expect("Current executable path getting error")); - - let mut command = Command::new(executable.as_path()); - command.stdin(std::process::Stdio::piped()); - command.stdout(std::process::Stdio::piped()); - command.stderr(std::process::Stdio::piped()); - command.arg("--recursive-process"); - command.arg(contract_name.path.as_str()); - - let mut process = command - .spawn() - .map_err(|error| anyhow::anyhow!("{executable:?} subprocess spawning error: {error:?}"))?; - - let stdin = process - .stdin - .as_mut() - .ok_or_else(|| anyhow::anyhow!("{executable:?} subprocess stdin getting error"))?; - let mut buffer = Vec::with_capacity(crate::r#const::DEFAULT_SERDE_BUFFER_SIZE); - ciborium::into_writer(input, &mut buffer).map_err(|error| { - anyhow::anyhow!("{executable:?} subprocess input serializing error: {error:?}") - })?; - stdin - .write_all(buffer.len().to_le_bytes().as_slice()) - .map_err(|error| { - anyhow::anyhow!("{executable:?} subprocess length prefix writing error: {error:?}") - })?; - stdin.write_all(buffer.as_slice()).map_err(|error| { - anyhow::anyhow!("{executable:?} subprocess input writing error: {error:?}") - })?; - - let result = process.wait_with_output().map_err(|error| { - anyhow::anyhow!("{executable:?} subprocess output reading error: {error:?}") - })?; - - if result.status.code() != Some(solx_utils::EXIT_CODE_SUCCESS) { - let message = format!( - "{executable:?} subprocess failed {}:\n{}\n{}", - match result.status.code() { - Some(code) => format!("with exit code {code:?}"), - None => "without exit code".to_owned(), - }, - String::from_utf8_lossy(result.stdout.as_slice()), - String::from_utf8_lossy(result.stderr.as_slice()), - ); - Err(solx_standard_json::OutputError::new_error_contract( - Some(contract_name.path.as_str()), - message, - ))?; - } - - if !result.stderr.is_empty() { - let stderr = std::io::stderr(); - let mut handle = stderr.lock(); - let _ = handle.write_all(result.stderr.as_slice()); - } - - ciborium::de::from_reader_with_recursion_limit(result.stdout.as_slice(), usize::MAX).map_err( - |error| { - anyhow::anyhow!( - "{executable:?} subprocess stdout deserializing error: {error:?}\n{}\n{}", - String::from_utf8_lossy(result.stdout.as_slice()), - String::from_utf8_lossy(result.stderr.as_slice()), - ) - }, - )? -} - -/// -/// 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. -/// -pub unsafe extern "C" fn evm_stack_error_handler(spill_area_size: u64) { - let result: Result = Err(Error::stack_too_deep( - spill_area_size, - solx_codegen_evm::IS_SIZE_FALLBACK.load(std::sync::atomic::Ordering::Relaxed), - )); - let mut buffer = Vec::with_capacity(crate::r#const::DEFAULT_SERDE_BUFFER_SIZE); - ciborium::into_writer(&result, &mut buffer) - .unwrap_or_else(|error| panic!("Stdout stack-too-deep error serializing error: {error}")); - std::io::stdout() - .write_all(buffer.as_slice()) - .unwrap_or_else(|error| panic!("Stdout stack-too-deep error writing error: {error}")); - std::io::Write::flush(&mut std::io::stdout()) - .unwrap_or_else(|error| panic!("Stdout flush error: {error}")); - unsafe { inkwell::support::shutdown_llvm() }; - std::process::exit(solx_utils::EXIT_CODE_SUCCESS); -} diff --git a/solx-core/src/process/output.rs b/solx-core/src/process/output.rs index 6ee6dc375..adc42fbee 100644 --- a/solx-core/src/process/output.rs +++ b/solx-core/src/process/output.rs @@ -1,6 +1,4 @@ //! -//! Process for compiling a single compilation unit. -//! //! The EVM output data. //! diff --git a/solx-core/src/process/pool.rs b/solx-core/src/process/pool.rs new file mode 100644 index 000000000..32a80531d --- /dev/null +++ b/solx-core/src/process/pool.rs @@ -0,0 +1,64 @@ +//! +//! The pool of persistent worker subprocesses. +//! + +use std::path::PathBuf; +use std::sync::Mutex; + +use crate::process::job::Job; +use crate::process::output::Output as EVMOutput; +use crate::process::session::Session; +use crate::process::worker::Worker; + +/// The lock-poisoning invariant shared by the idle-pool accessors. +const POISON: &str = "lock is never poisoned because worker threads do not panic"; + +/// +/// The pool of persistent worker subprocesses. +/// +/// Workers are spawned on demand and returned to the idle list after each successful job, +/// so the number of live workers never exceeds the number of dispatching threads. +/// +pub struct Pool { + /// The worker executable path. + executable: PathBuf, + /// The project-wide data sent to every spawned worker. + session: Session, + /// The idle workers available for checkout. + idle: Mutex>, +} + +impl Pool { + /// + /// Creates a pool that dispatches jobs of `session` to worker subprocesses. + /// + pub fn new(session: Session) -> anyhow::Result { + let executable = crate::process::EXECUTABLE + .get() + .cloned() + .unwrap_or_else(|| { + std::env::current_exe().expect("Current executable path getting error") + }); + Ok(Self { + executable, + session, + idle: Mutex::new(Vec::new()), + }) + } + + /// + /// Compiles one translation unit on a pooled or freshly spawned worker. + /// + /// On success the worker rejoins the pool; any error drops it through the early return. + /// + pub fn execute(&self, job: &Job) -> crate::Result { + let idle = self.idle.lock().expect(POISON).pop(); + let mut worker = match idle { + Some(worker) => worker, + None => Worker::spawn(self.executable.as_path(), &self.session)?, + }; + let output = worker.execute(job)?; + self.idle.lock().expect(POISON).push(worker); + Ok(output) + } +} diff --git a/solx-core/src/process/session.rs b/solx-core/src/process/session.rs new file mode 100644 index 000000000..c4af014aa --- /dev/null +++ b/solx-core/src/process/session.rs @@ -0,0 +1,47 @@ +//! +//! The project-wide data shared by all translation units. +//! + +/// +/// The project-wide data shared by all translation units. +/// +/// Sent to every worker subprocess once, before the per-unit jobs. +/// +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct Session { + /// The input contract language. + pub language: solx_standard_json::InputLanguage, + /// The `solc` compiler version, used only for Solidity and Yul projects. + pub solc_version: Option, + /// The EVM version to produce bytecode for. + pub evm_version: Option, + /// Output selection for the compilation. + pub output_selection: solx_standard_json::InputSelection, + /// The extra LLVM arguments. + pub llvm_options: Vec, + /// The output config for IR artifacts. + pub output_config: Option, +} + +impl Session { + /// + /// A shortcut constructor. + /// + pub fn new( + language: solx_standard_json::InputLanguage, + solc_version: Option, + evm_version: Option, + output_selection: solx_standard_json::InputSelection, + llvm_options: Vec, + output_config: Option, + ) -> Self { + Self { + language, + solc_version, + evm_version, + output_selection, + llvm_options, + output_config, + } + } +} diff --git a/solx-core/src/process/worker.rs b/solx-core/src/process/worker.rs new file mode 100644 index 000000000..ad06eaa40 --- /dev/null +++ b/solx-core/src/process/worker.rs @@ -0,0 +1,79 @@ +//! +//! A persistent worker subprocess owned by the pool. +//! + +use std::io::BufReader; +use std::path::Path; +use std::process::Child; +use std::process::ChildStdout; +use std::process::Command; +use std::process::Stdio; + +use crate::process::channel::FrameRead; +use crate::process::channel::FrameWrite; +use crate::process::job::Job; +use crate::process::output::Output as EVMOutput; +use crate::process::session::Session; + +/// +/// A persistent worker subprocess with its framed I/O channel. +/// +/// Its `stderr` is inherited, so subprocess diagnostics stream directly to the parent. +/// Dropping a worker closes its `stdin`, which the worker loop treats as a shutdown request. +/// +pub struct Worker { + /// The worker subprocess handle. + child: Child, + /// The buffered response stream. + stdout: BufReader, +} + +impl Worker { + /// + /// Spawns a worker subprocess and sends it the session frame. + /// + pub fn spawn(executable: &Path, session: &Session) -> anyhow::Result { + let mut command = Command::new(executable); + command.stdin(Stdio::piped()); + command.stdout(Stdio::piped()); + command.stderr(Stdio::inherit()); + command.arg("--recursive-process"); + + let mut child = command.spawn().map_err(|error| { + anyhow::anyhow!("{executable:?} subprocess spawning error: {error:?}") + })?; + child + .stdin + .as_mut() + .expect("The worker stdin is always piped") + .send(session)?; + let stdout = BufReader::new( + child + .stdout + .take() + .expect("The worker stdout is always piped"), + ); + Ok(Self { child, stdout }) + } + + /// + /// Sends `job` to the worker and returns the compilation result it replies with. + /// + pub fn execute(&mut self, job: &Job) -> crate::Result { + self.child + .stdin + .as_mut() + .expect("The worker stdin is always piped") + .send(job)?; + self.stdout + .recv::>()? + .ok_or_else(|| anyhow::anyhow!("The worker closed the response channel"))? + } +} + +impl Drop for Worker { + fn drop(&mut self) { + drop(self.child.stdin.take()); + let _ = self.child.wait(); + } +} diff --git a/solx-core/src/project/contract/mod.rs b/solx-core/src/project/contract/mod.rs index f3b3416c9..efbf7c5ed 100644 --- a/solx-core/src/project/contract/mod.rs +++ b/solx-core/src/project/contract/mod.rs @@ -126,7 +126,7 @@ impl Contract { code_segment: solx_utils::CodeSegment, evm_version: Option, debug_info: Option, - output_selection: solx_standard_json::InputSelection, + output_selection: &solx_standard_json::InputSelection, immutables: Option>>, metadata_bytes: Option>, mut optimizer_settings: solx_codegen_evm::OptimizerSettings, @@ -202,9 +202,6 @@ impl Contract { solidity_data, output_config, ); - inkwell::support::error_handling::install_stack_error_handler( - crate::process::evm_stack_error_handler, - ); let run_yul_lowering = profiler.start_evm_translation_unit( contract_name.full_path.as_str(), code_segment, @@ -341,9 +338,6 @@ impl Contract { solidity_data, output_config, ); - inkwell::support::error_handling::install_stack_error_handler( - crate::process::evm_stack_error_handler, - ); context.set_evmla_data(evmla_data); context.set_capture_evmla(output_selection.check_selection( contract_name.path.as_str(), @@ -462,9 +456,6 @@ impl Contract { None, output_config, ); - inkwell::support::error_handling::install_stack_error_handler( - crate::process::evm_stack_error_handler, - ); if output_selection.check_selection( contract_name.path.as_str(), contract_name.name.as_deref(), @@ -574,9 +565,6 @@ impl Contract { None, output_config, ); - inkwell::support::error_handling::install_stack_error_handler( - crate::process::evm_stack_error_handler, - ); if output_selection.check_selection( contract_name.path.as_str(), contract_name.name.as_deref(), diff --git a/solx-core/src/project/mod.rs b/solx-core/src/project/mod.rs index 6365f07de..c247f654d 100644 --- a/solx-core/src/project/mod.rs +++ b/solx-core/src/project/mod.rs @@ -16,8 +16,10 @@ use rayon::iter::ParallelIterator; use crate::build::Build as EVMBuild; use crate::build::contract::Contract as EVMContractBuild; use crate::error::Error; -use crate::process::input::Input as EVMProcessInput; +use crate::process::job::Job as EVMProcessJob; use crate::process::output::Output as EVMProcessOutput; +use crate::process::pool::Pool as EVMProcessPool; +use crate::process::session::Session as EVMProcessSession; use self::contract::Contract; use self::contract::ir::IR as ContractIR; @@ -515,7 +517,24 @@ impl Project { llvm_options: Vec, output_config: Option, ) -> anyhow::Result { - let mut contracts: Vec<(String, Contract)> = self.contracts.into_iter().collect(); + let Self { + language, + solc_version, + contracts, + ast_jsons, + libraries: _, + debug_info, + } = self; + let pool = EVMProcessPool::new(EVMProcessSession::new( + language, + solc_version.clone(), + evm_version, + output_selection.clone(), + llvm_options.clone(), + output_config, + ))?; + + let mut contracts: Vec<(String, Contract)> = contracts.into_iter().collect(); contracts.sort_unstable_by(|(_, left), (_, right)| { right .estimated_compilation_cost() @@ -548,7 +567,7 @@ impl Project { let runtime_code: ContractYul = *deploy_code.runtime_code.take().expect("Always exists"); - deploy_debug_info = self.debug_info.as_ref().and_then(|debug_info| { + deploy_debug_info = debug_info.as_ref().and_then(|debug_info| { output_selection .check_selection( path.as_str(), @@ -562,7 +581,7 @@ impl Project { ) }) }); - runtime_debug_info = self.debug_info.as_ref().and_then(|debug_info| { + runtime_debug_info = debug_info.as_ref().and_then(|debug_info| { output_selection .check_selection( path.as_str(), @@ -583,7 +602,7 @@ impl Project { let runtime_code: ContractEVMLegacyAssembly = *deploy_code.runtime_code.take().expect("Always exists"); - deploy_debug_info = self.debug_info.as_ref().and_then(|debug_info| { + deploy_debug_info = debug_info.as_ref().and_then(|debug_info| { output_selection .check_selection( path.as_str(), @@ -597,7 +616,7 @@ impl Project { ) }) }); - runtime_debug_info = self.debug_info.as_ref().and_then(|debug_info| { + runtime_debug_info = debug_info.as_ref().and_then(|debug_info| { output_selection .check_selection( path.as_str(), @@ -661,30 +680,24 @@ impl Project { let (runtime_object_result, metadata) = { let metadata_bytes = Self::cbor_metadata( metadata.as_deref(), - self.solc_version.as_ref(), + solc_version.as_ref(), &optimizer_settings, llvm_options.as_slice(), metadata_hash_type, append_cbor, ); - let mut input = EVMProcessInput::new( - self.language, - self.solc_version.clone(), + let mut job = EVMProcessJob::new( contract_name.clone(), runtime_code_ir, solx_utils::CodeSegment::Runtime, - evm_version, runtime_debug_info, - output_selection.to_owned(), None, metadata_bytes, optimizer_settings.clone(), - llvm_options.clone(), - output_config.clone(), ); - let result = Self::run_multi_pass_pipeline(&contract_name, &mut input); + let result = Self::run_multi_pass_pipeline(&pool, &mut job); (result, metadata) }; @@ -693,23 +706,17 @@ impl Project { .ok() .and_then(|output| output.object.immutables.to_owned()); let deploy_object_result: crate::Result = { - let mut input = EVMProcessInput::new( - self.language, - self.solc_version.clone(), + let mut job = EVMProcessJob::new( contract_name.clone(), deploy_code_ir, solx_utils::CodeSegment::Deploy, - evm_version, deploy_debug_info, - output_selection.to_owned(), immutables, None, optimizer_settings.clone(), - llvm_options.clone(), - output_config.clone(), ); - Self::run_multi_pass_pipeline(&contract_name, &mut input) + Self::run_multi_pass_pipeline(&pool, &mut job) }; let build = EVMContractBuild::new( @@ -734,7 +741,7 @@ impl Project { }) .collect::>(); - Ok(EVMBuild::new(results, self.ast_jsons, messages)) + Ok(EVMBuild::new(results, ast_jsons, messages)) } /// @@ -806,15 +813,13 @@ impl Project { /// and turning on the size fallback to overcome the EVM bytecode size limit. /// fn run_multi_pass_pipeline( - contract_name: &solx_utils::ContractName, - input: &mut EVMProcessInput, + pool: &EVMProcessPool, + job: &mut EVMProcessJob, ) -> crate::Result { - let mut result: crate::Result; let mut pass_count = 0; loop { - result = crate::process::call(contract_name, input); pass_count += 1; - match result { + match pool.execute(job) { Err(Error::StackTooDeep(stack_too_deep)) => { assert!( pass_count <= 2, @@ -822,17 +827,13 @@ impl Project { ); if stack_too_deep.is_size_fallback { - input.optimizer_settings.switch_to_size_fallback(); + job.optimizer_settings.switch_to_size_fallback(); } - input - .optimizer_settings + job.optimizer_settings .set_spill_area_size(stack_too_deep.spill_area_size); - - continue; } - _ => break, + result => break result, } } - result } } diff --git a/solx-llvm b/solx-llvm index cec9acbdc..9537dbed1 160000 --- a/solx-llvm +++ b/solx-llvm @@ -1 +1 @@ -Subproject commit cec9acbdc2799e5607ea57c4f1617ca8579afbf9 +Subproject commit 9537dbed1cab68b483aba62a4560ba6a9dd78231 diff --git a/solx-solc-test-adapter/src/test/function_call/parser/lexical/stream/integer/mod.rs b/solx-solc-test-adapter/src/test/function_call/parser/lexical/stream/integer/mod.rs index 762bbbf2c..3a3ffef6d 100644 --- a/solx-solc-test-adapter/src/test/function_call/parser/lexical/stream/integer/mod.rs +++ b/solx-solc-test-adapter/src/test/function_call/parser/lexical/stream/integer/mod.rs @@ -72,13 +72,12 @@ pub fn parse(input: &str) -> Result { }, State::Minus => match character { Some(character) => { - if Integer::CHARACTERS_DECIMAL.contains(&character) { - integer.push(character); - size += 1; - state = State::Decimal; - } else { + if !Integer::CHARACTERS_DECIMAL.contains(&character) { return Err(Error::NotAnInteger); } + integer.push(character); + size += 1; + state = State::Decimal; } None => return Err(Error::NotAnInteger), }, diff --git a/solx/tests/cli/recursive_process.rs b/solx/tests/cli/recursive_process.rs index 019309c99..c92ca8934 100644 --- a/solx/tests/cli/recursive_process.rs +++ b/solx/tests/cli/recursive_process.rs @@ -11,9 +11,9 @@ fn missing_input() -> anyhow::Result<()> { let args = &["--recursive-process"]; let result = crate::cli::execute_solx(args)?; - result.failure().stderr(predicate::str::contains( - "Input length prefix reading error: failed to fill whole buffer", - )); + result + .failure() + .stderr(predicate::str::contains("The worker received no session")); Ok(()) } @@ -22,11 +22,7 @@ fn missing_input() -> anyhow::Result<()> { fn excess_args() -> anyhow::Result<()> { crate::common::setup()?; - let args = &[ - "--recursive-process", - crate::common::TEST_SOLIDITY_CONTRACT, - "excess", - ]; + let args = &["--recursive-process", crate::common::TEST_SOLIDITY_CONTRACT]; let result = crate::cli::execute_solx(args)?; result.failure().stderr(predicate::str::contains(