diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c53f0d7..70f647dc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,13 +28,10 @@ jobs: include: - name: ubuntu-latest / stable os: ubuntu-latest - native_aot_smoke_target: "" - name: macos-14 / stable os: macos-14 - native_aot_smoke_target: x86_64-apple-darwin - name: windows-latest / stable os: windows-latest - native_aot_smoke_target: "" steps: - name: Checkout code uses: actions/checkout@v7 @@ -44,10 +41,6 @@ jobs: with: components: rustfmt, clippy - - name: Install Native AoT smoke target - if: ${{ matrix.native_aot_smoke_target != '' }} - run: rustup target add ${{ matrix.native_aot_smoke_target }} - - name: Setup cache uses: Swatinem/rust-cache@v2 with: @@ -75,8 +68,6 @@ jobs: run: cargo test - name: Run tests (all features) - env: - PHARMSOL_NATIVE_AOT_SMOKE_TARGET: ${{ matrix.native_aot_smoke_target }} run: cargo test --all-features - name: Run doc tests diff --git a/.gitignore b/.gitignore index f6555856..1c8fd493 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,7 @@ cargo.lock Cargo.lock /.vscode /.idea -*.pkm /paper_files paper.html /joss/paper_files -/tests/browser-e2e/node_modules/ docs/ diff --git a/Cargo.toml b/Cargo.toml index 5bee1ef8..b4f06bac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,13 +37,10 @@ dsl-jit = [ "dep:cranelift-module", "dep:cranelift-native", ] -dsl-aot = ["dsl-core"] -dsl-aot-load = ["dsl-core", "dep:libloading"] [dependencies] pharmsol-dsl = { workspace = true } pharmsol-macros = { workspace = true } -libloading = { version = "0.9.0", optional = true, features = [] } cranelift = { version = "0.134.3", optional = true } cranelift-jit = { version = "0.134.3", optional = true } cranelift-module = { version = "0.134.3", optional = true } @@ -70,8 +67,6 @@ quick_cache = "0.7.0" criterion = { version = "0.8.2", features = ["html_reports"] } approx = "0.5.1" tempfile = "3.27.0" -tiny_http = "0.12.0" -webbrowser = "1.2.1" [lib] bench = false @@ -83,4 +78,4 @@ harness = false [[bench]] name = "dsl_matrix" harness = false -required-features = ["dsl-jit", "dsl-aot", "dsl-aot-load"] +required-features = ["dsl-jit"] diff --git a/README.md b/README.md index 9306158e..83962d11 100644 --- a/README.md +++ b/README.md @@ -110,18 +110,13 @@ see [docs/analytical-authoring-migration.md](docs/analytical-authoring-migration ## DSL and Runtime Targets If the model needs to be loaded or compiled at runtime, pharmsol also provides a DSL with -the same broad modeling coverage: ODE, analytical, and SDE authoring. The DSL can target -an in-process JIT runtime or native ahead-of-time artifacts depending on how you want to -ship and execute the model. - -- `dsl-jit`: compile DSL source into a runtime model inside the current process. -- `dsl-aot` and `dsl-aot-load`: emit a native artifact and load it later. +the same broad modeling coverage: ODE, analytical, and SDE authoring. Enable the `dsl-jit` +feature to compile DSL source into a runtime model inside the current process. See [examples/dsl_runtime_jit.rs](examples/dsl_runtime_jit.rs) for the in-repo JIT flow and [examples/dsl_jit_analytical_covariates.rs](examples/dsl_jit_analytical_covariates.rs) for a small analytical covariate example written both as DSL JIT source and as an `analytical!` model. -The companion `pharmsol-examples` crate includes an end-to-end native AOT runtime example. ## Performance diff --git a/benches/dsl_matrix.rs b/benches/dsl_matrix.rs index 83ea1804..5b19bad6 100644 --- a/benches/dsl_matrix.rs +++ b/benches/dsl_matrix.rs @@ -1,4 +1,4 @@ -//! DSL bench matrix (feature-gated): JIT, native AoT across all workloads + solvers. +//! DSL bench matrix (feature-gated): JIT across all workloads + solvers. //! Mirrors `native_matrix.rs` but compiles models from DSL source. //! //! IDs: @@ -8,15 +8,13 @@ //! - `dsl/likelihood-matrix` → `{workload}/{kind}/{backend}` use std::hint::black_box; -use std::path::PathBuf; use std::time::Duration; use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, SamplingMode}; -use tempfile::TempDir; use pharmsol::dsl::{ - compile_module_source_to_runtime, CompiledRuntimeModel, NativeAnalyticalModel, - NativeAotCompileOptions, NativeOdeModel, NativeSdeModel, RuntimeCompilationTarget, + compile_module_source_to_runtime, CompiledRuntimeModel, NativeAnalyticalModel, NativeOdeModel, + NativeSdeModel, RuntimeCompilationTarget, }; use pharmsol::prelude::*; use pharmsol::{Cache, Parameters}; @@ -31,21 +29,17 @@ const MATRIX_N_SUBJECTS: usize = 32; const MATRIX_N_SUPPORT: usize = 64; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[allow(dead_code)] // Aot temporarily disabled in `Backend::all` enum Backend { Jit, - Aot, } impl Backend { fn label(self) -> &'static str { match self { Self::Jit => "dsl-jit", - Self::Aot => "dsl-aot", } } - // AoT backend temporarily disabled — too slow for the current matrix. fn all() -> [Backend; 1] { [Backend::Jit] } @@ -70,52 +64,19 @@ impl CacheState { } } -/// One `TempDir` shared across the bench binary; each compile gets a fresh subdir. -struct AotWorkspace { - root: TempDir, - counter: std::cell::Cell, -} - -impl AotWorkspace { - fn new() -> Self { - Self { - root: tempfile::Builder::new() - .prefix("pharmsol-bench-dsl-aot-") - .tempdir() - .expect("create AoT workspace tempdir"), - counter: std::cell::Cell::new(0), - } - } - - fn fresh(&self, stem: &str) -> PathBuf { - let n = self.counter.get(); - self.counter.set(n + 1); - self.root.path().join(format!("{stem}-{n:04}")) - } -} - /// Compile `(workload, kind)` with `backend` and return the full `CompiledRuntimeModel`. -fn compile_runtime( - workload: Workload, - kind: SolverKind, - backend: Backend, - aot: &AotWorkspace, -) -> CompiledRuntimeModel { +fn compile_runtime(workload: Workload, kind: SolverKind, backend: Backend) -> CompiledRuntimeModel { let source = dsl_source(workload, kind); let name = dsl_model_name(workload, kind); let target = match backend { Backend::Jit => RuntimeCompilationTarget::Jit, - Backend::Aot => { - let dir = aot.fresh(&format!("{}-{}", workload.label(), kind.label())); - RuntimeCompilationTarget::NativeAot(NativeAotCompileOptions::new(dir)) - } }; compile_module_source_to_runtime(source, Some(name), target, |_, _| {}) .unwrap_or_else(|e| panic!("compile {} via {} failed: {e:?}", name, backend.label())) } -fn compile_ode(workload: Workload, backend: Backend, aot: &AotWorkspace) -> NativeOdeModel { - match compile_runtime(workload, SolverKind::Ode, backend, aot) { +fn compile_ode(workload: Workload, backend: Backend) -> NativeOdeModel { + match compile_runtime(workload, SolverKind::Ode, backend) { CompiledRuntimeModel::Ode(model) => model, other => panic!( "expected Ode model for {}, got {:?}", @@ -125,12 +86,8 @@ fn compile_ode(workload: Workload, backend: Backend, aot: &AotWorkspace) -> Nati } } -fn compile_analytical( - workload: Workload, - backend: Backend, - aot: &AotWorkspace, -) -> NativeAnalyticalModel { - match compile_runtime(workload, SolverKind::Analytical, backend, aot) { +fn compile_analytical(workload: Workload, backend: Backend) -> NativeAnalyticalModel { + match compile_runtime(workload, SolverKind::Analytical, backend) { CompiledRuntimeModel::Analytical(model) => model, other => panic!( "expected Analytical model for {}, got {:?}", @@ -140,8 +97,8 @@ fn compile_analytical( } } -fn compile_sde(workload: Workload, backend: Backend, aot: &AotWorkspace) -> NativeSdeModel { - match compile_runtime(workload, SolverKind::Sde, backend, aot) { +fn compile_sde(workload: Workload, backend: Backend) -> NativeSdeModel { + match compile_runtime(workload, SolverKind::Sde, backend) { CompiledRuntimeModel::Sde(model) => model, other => panic!( "expected Sde model for {}, got {:?}", @@ -175,16 +132,14 @@ fn compile_group(c: &mut Criterion) { group.sampling_mode(SamplingMode::Flat); group.sample_size(10); group.measurement_time(Duration::from_secs(5)); - // Each compile leaks an executable mmap (JIT) or runs rustc (AoT). Without - // a cap, a fast JIT compile (~60 µs) lets Criterion request hundreds of - // thousands of iterations per cell and exhausts the runner's executable - // memory pool / `vm.max_map_count`. We hard-cap real iterations per - // Criterion batch to `MAX_ITERS_PER_BATCH` and scale the reported elapsed - // time linearly so per-iteration timings stay accurate. + // Each compile leaks an executable mmap. Without a cap, a fast JIT compile + // (~60 µs) lets Criterion request hundreds of thousands of iterations per + // cell and exhausts the runner's executable memory pool / + // `vm.max_map_count`. We hard-cap real iterations per Criterion batch to + // `MAX_ITERS_PER_BATCH` and scale the reported elapsed time linearly so + // per-iteration timings stay accurate. const MAX_ITERS_PER_BATCH: u64 = 25; - let aot = AotWorkspace::new(); - for workload in Workload::all() { for kind in SolverKind::all() { for backend in Backend::all() { @@ -203,7 +158,6 @@ fn compile_group(c: &mut Criterion) { black_box(workload), black_box(kind), black_box(backend), - &aot, )); } let elapsed = start.elapsed(); @@ -223,8 +177,6 @@ fn predictions_group(c: &mut Criterion) { let mut group = c.benchmark_group("dsl/predictions"); group.sampling_mode(SamplingMode::Flat); - let aot = AotWorkspace::new(); - for workload in Workload::all() { let subject = subject_for_predictions(workload); for kind in SolverKind::all() { @@ -240,10 +192,8 @@ fn predictions_group(c: &mut Criterion) { match kind { SolverKind::Ode => { let model = match cache { - CacheState::Hot => compile_ode(workload, backend, &aot), - CacheState::Cold => { - compile_ode(workload, backend, &aot).disable_cache() - } + CacheState::Hot => compile_ode(workload, backend), + CacheState::Cold => compile_ode(workload, backend).disable_cache(), }; let theta = ode_parameters(&model, workload); group.bench_function(bench_id, |b| { @@ -261,9 +211,9 @@ fn predictions_group(c: &mut Criterion) { } SolverKind::Analytical => { let model = match cache { - CacheState::Hot => compile_analytical(workload, backend, &aot), + CacheState::Hot => compile_analytical(workload, backend), CacheState::Cold => { - compile_analytical(workload, backend, &aot).disable_cache() + compile_analytical(workload, backend).disable_cache() } }; let theta = analytical_parameters(&model, workload); @@ -282,10 +232,8 @@ fn predictions_group(c: &mut Criterion) { } SolverKind::Sde => { let model = match cache { - CacheState::Hot => compile_sde(workload, backend, &aot), - CacheState::Cold => { - compile_sde(workload, backend, &aot).disable_cache() - } + CacheState::Hot => compile_sde(workload, backend), + CacheState::Cold => compile_sde(workload, backend).disable_cache(), }; let theta = sde_parameters(&model, workload); group.bench_function(bench_id, |b| { @@ -316,7 +264,6 @@ fn log_likelihood_group(c: &mut Criterion) { let mut group = c.benchmark_group("dsl/log-likelihood"); group.sampling_mode(SamplingMode::Flat); - let aot = AotWorkspace::new(); let error_models = assay_error_models(); for workload in Workload::all() { @@ -334,10 +281,8 @@ fn log_likelihood_group(c: &mut Criterion) { match kind { SolverKind::Ode => { let model = match cache { - CacheState::Hot => compile_ode(workload, backend, &aot), - CacheState::Cold => { - compile_ode(workload, backend, &aot).disable_cache() - } + CacheState::Hot => compile_ode(workload, backend), + CacheState::Cold => compile_ode(workload, backend).disable_cache(), }; let theta = ode_parameters(&model, workload); group.bench_function(bench_id, |b| { @@ -356,9 +301,9 @@ fn log_likelihood_group(c: &mut Criterion) { } SolverKind::Analytical => { let model = match cache { - CacheState::Hot => compile_analytical(workload, backend, &aot), + CacheState::Hot => compile_analytical(workload, backend), CacheState::Cold => { - compile_analytical(workload, backend, &aot).disable_cache() + compile_analytical(workload, backend).disable_cache() } }; let theta = analytical_parameters(&model, workload); @@ -378,10 +323,8 @@ fn log_likelihood_group(c: &mut Criterion) { } SolverKind::Sde => { let model = match cache { - CacheState::Hot => compile_sde(workload, backend, &aot), - CacheState::Cold => { - compile_sde(workload, backend, &aot).disable_cache() - } + CacheState::Hot => compile_sde(workload, backend), + CacheState::Cold => compile_sde(workload, backend).disable_cache(), }; let theta = sde_parameters(&model, workload); group.bench_function(bench_id, |b| { @@ -417,7 +360,6 @@ fn likelihood_matrix_group(c: &mut Criterion) { group.sample_size(10); group.measurement_time(Duration::from_secs(20)); - let aot = AotWorkspace::new(); let error_models = assay_error_models(); for workload in Workload::all() { @@ -433,7 +375,7 @@ fn likelihood_matrix_group(c: &mut Criterion) { )); match kind { SolverKind::Ode => { - let model = compile_ode(workload, backend, &aot); + let model = compile_ode(workload, backend); group.bench_function(bench_id, |b| { b.iter(|| { black_box( @@ -450,7 +392,7 @@ fn likelihood_matrix_group(c: &mut Criterion) { }); } SolverKind::Analytical => { - let model = compile_analytical(workload, backend, &aot); + let model = compile_analytical(workload, backend); group.bench_function(bench_id, |b| { b.iter(|| { black_box( @@ -467,7 +409,7 @@ fn likelihood_matrix_group(c: &mut Criterion) { }); } SolverKind::Sde => { - let model = compile_sde(workload, backend, &aot); + let model = compile_sde(workload, backend); group.bench_function(bench_id, |b| { b.iter(|| { black_box( diff --git a/pharmsol-dsl/README.md b/pharmsol-dsl/README.md index 38a7cdc1..131e91fb 100644 --- a/pharmsol-dsl/README.md +++ b/pharmsol-dsl/README.md @@ -9,7 +9,7 @@ Use this crate when you need to work with model source as data: - analyze names and types into a checked model - compile validated models into the ready-to-run form used by runtime backends -Do not use this crate for JIT compilation, native AoT export or load, or `Subject`-based prediction helpers. Those workflows stay in `pharmsol::dsl` in the main `pharmsol` crate. +Do not use this crate for JIT compilation or `Subject`-based prediction helpers. Those workflows stay in `pharmsol::dsl` in the main `pharmsol` crate. ## Main Pipeline @@ -51,7 +51,7 @@ The main public modules are: - `syntax` for the syntax tree - `diagnostic` for spans, codes, and rendered reports - `analysis` for the analyzed, fully checked model -- `execution` for the ready-to-run model shared by JIT and AoT backends +- `execution` for the ready-to-run model consumed by the runtime backend The parser accepts both canonical `model { ... }` source and the authoring shorthand used by the `pharmsol` examples. diff --git a/pharmsol-dsl/src/lib.rs b/pharmsol-dsl/src/lib.rs index 961ffe98..30f8853c 100644 --- a/pharmsol-dsl/src/lib.rs +++ b/pharmsol-dsl/src/lib.rs @@ -42,8 +42,7 @@ //! - [`syntax`] for the syntax tree. //! - [`analysis`] for the analyzed, fully checked model. //! - [`diagnostic`] for spans, diagnostic codes, and rendered reports. -//! - [`execution`] for the ready-to-run model shared by the JIT and AoT -//! backends. +//! - [`execution`] for the ready-to-run model consumed by the JIT backend. //! //! Smallest one-shot example: //! diff --git a/src/build_support.rs b/src/build_support.rs deleted file mode 100644 index bdcf2c21..00000000 --- a/src/build_support.rs +++ /dev/null @@ -1,342 +0,0 @@ -use std::env; -use std::fs; -use std::io; -use std::io::Read; -use std::path::{Path, PathBuf}; -use std::process::{Command, Stdio}; -use std::sync::Arc; -use std::thread; - -#[cfg(windows)] -use std::os::windows::process::CommandExt; - -#[cfg(windows)] -const CREATE_NO_WINDOW: u32 = 0x08000000; - -#[allow(unused_mut)] -fn new_command(program: &str) -> Command { - let mut cmd = Command::new(program); - #[cfg(windows)] - cmd.creation_flags(CREATE_NO_WINDOW); - cmd -} - -pub(crate) fn find_cargo() -> String { - if let Ok(output) = Command::new("cargo").arg("--version").output() { - if output.status.success() { - return "cargo".to_string(); - } - } - - if let Ok(cargo_home) = env::var("CARGO_HOME") { - let cargo_path = PathBuf::from(&cargo_home) - .join("bin") - .join(cargo_exe_name()); - if cargo_path.exists() { - return cargo_path.to_string_lossy().to_string(); - } - } - - let home = env::var("HOME") - .or_else(|_| env::var("USERPROFILE")) - .unwrap_or_default(); - - if !home.is_empty() { - let standard_path = PathBuf::from(&home) - .join(".cargo") - .join("bin") - .join(cargo_exe_name()); - if standard_path.exists() { - return standard_path.to_string_lossy().to_string(); - } - } - - #[cfg(target_os = "windows")] - { - let candidates = [ - "C:\\Program Files\\Rust stable MSVC\\bin\\cargo.exe", - "C:\\Program Files\\Rust stable GNU\\bin\\cargo.exe", - ]; - for candidate in &candidates { - if PathBuf::from(candidate).exists() { - return candidate.to_string(); - } - } - } - - #[cfg(target_os = "macos")] - { - let candidates = ["/opt/homebrew/bin/cargo", "/usr/local/bin/cargo"]; - for candidate in &candidates { - if PathBuf::from(candidate).exists() { - return candidate.to_string(); - } - } - } - - #[cfg(target_os = "linux")] - { - let candidates = ["/usr/local/bin/cargo", "/usr/bin/cargo", "/snap/bin/cargo"]; - for candidate in &candidates { - if PathBuf::from(candidate).exists() { - return candidate.to_string(); - } - } - } - - "cargo".to_string() -} - -#[cfg(test)] -pub(crate) fn find_rustup() -> Option { - if let Ok(output) = Command::new("rustup").arg("--version").output() { - if output.status.success() { - return Some("rustup".to_string()); - } - } - - if let Ok(cargo_home) = env::var("CARGO_HOME") { - let rustup_path = PathBuf::from(&cargo_home) - .join("bin") - .join(rust_tool_exe_name("rustup")); - if rustup_path.exists() { - return Some(rustup_path.to_string_lossy().to_string()); - } - } - - let home = env::var("HOME") - .or_else(|_| env::var("USERPROFILE")) - .unwrap_or_default(); - - if !home.is_empty() { - let standard_path = PathBuf::from(&home) - .join(".cargo") - .join("bin") - .join(rust_tool_exe_name("rustup")); - if standard_path.exists() { - return Some(standard_path.to_string_lossy().to_string()); - } - } - - None -} - -#[cfg(test)] -pub(crate) fn rustc_host_target() -> Result { - let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string()); - let output = new_command(&rustc).arg("-vV").output()?; - if !output.status.success() { - return Err(io::Error::other("failed to run `rustc -vV`")); - } - - String::from_utf8_lossy(&output.stdout) - .lines() - .find_map(|line| line.strip_prefix("host: ")) - .map(|line| line.trim().to_string()) - .ok_or_else(|| io::Error::other("`rustc -vV` did not report a host target")) -} - -#[cfg(test)] -pub(crate) fn rustup_installed_targets() -> Result, io::Error> { - let rustup = find_rustup() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "rustup was not found"))?; - let output = new_command(&rustup) - .args(["target", "list", "--installed"]) - .output()?; - if !output.status.success() { - return Err(io::Error::other( - "failed to run `rustup target list --installed`", - )); - } - - Ok(String::from_utf8_lossy(&output.stdout) - .lines() - .map(str::trim) - .filter(|line| !line.is_empty()) - .map(ToOwned::to_owned) - .collect()) -} - -fn cargo_exe_name() -> &'static str { - rust_tool_exe_name("cargo") -} - -fn rust_tool_exe_name(tool: &'static str) -> &'static str { - #[cfg(target_os = "windows")] - { - match tool { - "cargo" => "cargo.exe", - "rustup" => "rustup.exe", - _ => tool, - } - } - #[cfg(not(target_os = "windows"))] - { - tool - } -} - -pub(crate) fn create_cargo_template( - temp_dir: PathBuf, - cargo_toml_content: &str, -) -> Result { - if !temp_dir.exists() { - fs::create_dir_all(&temp_dir)?; - } - - let template_dir = temp_dir.join("template"); - let cargo_toml_path = template_dir.join("Cargo.toml"); - let src_dir = template_dir.join("src"); - let needs_scaffold = !template_dir.exists() || !src_dir.exists(); - - if needs_scaffold { - if template_dir.exists() { - fs::remove_dir_all(&template_dir)?; - } - fs::create_dir_all(&src_dir)?; - fs::write(&cargo_toml_path, cargo_toml_content)?; - } else if !cargo_toml_path.exists() { - fs::write(&cargo_toml_path, cargo_toml_content)?; - } else { - let existing_content = fs::read_to_string(&cargo_toml_path)?; - if existing_content.trim() != cargo_toml_content.trim() { - tracing::info!("template manifest changed, invalidating generated artifact cache"); - fs::write(&cargo_toml_path, cargo_toml_content)?; - let target_dir = template_dir.join("target"); - if target_dir.exists() { - fs::remove_dir_all(&target_dir)?; - } - } - } - - Ok(template_dir) -} - -pub(crate) fn write_template_source( - template_dir: impl AsRef, - source: &str, -) -> Result<(), io::Error> { - let template_dir = template_dir.as_ref(); - fs::write(template_dir.join("src").join("lib.rs"), source)?; - - let cargo_path = find_cargo(); - let _ = new_command(&cargo_path) - .arg("fmt") - .current_dir(template_dir) - .output(); - Ok(()) -} - -pub(crate) fn build_cargo_template( - template_path: PathBuf, - event_callback: Arc, - backend: &'static str, - model_name: String, - target: Option<&str>, - artifact_path: &[&str], -) -> Result { - let cargo_path = find_cargo(); - let target_dir = template_path.join("target"); - - let mut started_message = format!( - "Compiling {backend} model `{}` in {}", - model_name, - template_path.display() - ); - if let Some(target) = target { - started_message.push_str(&format!(" for target `{target}`")); - } - event_callback("started".into(), started_message); - - let mut command = new_command(&cargo_path); - command - .arg("build") - .arg("--release") - // .arg("--quiet") - .arg("--target-dir") - .arg(&target_dir) - .current_dir(&template_path) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()); - - if let Some(target) = target { - command.arg("--target").arg(target); - } - - let mut child = command.spawn()?; - let stdout = child.stdout.take().expect("Failed to capture stdout"); - let stderr = child.stderr.take().expect("Failed to capture stderr"); - - let stdout_handle = stream_output(stdout, event_callback.clone(), model_name.clone()); - let stderr_handle = stream_output(stderr, event_callback.clone(), model_name); - - let status = child.wait()?; - stdout_handle - .join() - .expect("Failed to join stdout thread")?; - stderr_handle - .join() - .expect("Failed to join stderr thread")?; - - if !status.success() { - return Err(io::Error::other("Failed to build the template")); - } - - let mut output_path = target_dir; - for segment in artifact_path { - output_path = output_path.join(segment); - } - Ok(output_path) -} - -#[cfg(feature = "dsl-aot")] -pub(crate) fn native_cdylib_filename_for_target( - crate_name: &str, - cargo_target: Option<&str>, -) -> String { - if target_uses_windows_dll(cargo_target) { - format!("{crate_name}.dll") - } else if target_uses_apple_dylib(cargo_target) { - format!("lib{crate_name}.dylib") - } else { - format!("lib{crate_name}.so") - } -} - -#[cfg(feature = "dsl-aot")] -fn target_uses_windows_dll(cargo_target: Option<&str>) -> bool { - cargo_target.map_or(cfg!(target_os = "windows"), |target| { - target.contains("windows") - }) -} - -#[cfg(feature = "dsl-aot")] -fn target_uses_apple_dylib(cargo_target: Option<&str>) -> bool { - cargo_target.map_or(cfg!(target_os = "macos"), |target| { - target.contains("apple") || target.contains("darwin") || target.contains("ios") - }) -} - -fn stream_output( - reader: R, - event_callback: Arc, - model_name: String, -) -> thread::JoinHandle> { - thread::spawn(move || { - let mut buffer = [0; 4096]; - let mut reader = io::BufReader::new(reader); - - loop { - let n = reader.read(&mut buffer)?; - if n == 0 { - break; - } - - let output = String::from_utf8_lossy(&buffer[..n]).to_string(); - let _ = &model_name; - event_callback("log".into(), output); - } - - Ok(()) - }) -} diff --git a/src/dsl/aot.rs b/src/dsl/aot.rs deleted file mode 100644 index 7e5bf462..00000000 --- a/src/dsl/aot.rs +++ /dev/null @@ -1,872 +0,0 @@ -use std::fmt; -#[cfg(feature = "dsl-aot")] -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; -#[cfg(feature = "dsl-aot")] -use std::sync::Arc; - -#[cfg(feature = "dsl-aot-load")] -use libloading::{Library, Symbol}; -#[cfg(feature = "dsl-aot")] -use rand::RngExt; -#[cfg(feature = "dsl-aot")] -use rand_distr::Alphanumeric; -use serde_json; -use thiserror::Error; - -use super::compiled_backend_abi::{ - decode_compiled_model_info, API_VERSION_SYMBOL, DERIVE_SYMBOL, DIFFUSION_SYMBOL, DRIFT_SYMBOL, - DYNAMICS_SYMBOL, INIT_SYMBOL, MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, - OUTPUTS_SYMBOL, ROUTE_BIOAVAILABILITY_SYMBOL, ROUTE_LAG_SYMBOL, -}; -#[cfg(feature = "dsl-aot-load")] -use super::native::{ - CompiledModelFunction, CompiledNativeModel, NativeExecutionArtifact, NativeModelInfo, -}; -#[cfg(feature = "dsl-aot")] -use super::rust_backend::{emit_rust_backend_source, RustBackendFlavor}; -#[cfg(feature = "dsl-aot")] -use crate::build_support::{ - build_cargo_template, create_cargo_template, native_cdylib_filename_for_target, - write_template_source, -}; -#[cfg(all(test, feature = "dsl-aot"))] -use crate::build_support::{rustc_host_target, rustup_installed_targets}; -#[cfg(feature = "dsl-aot-load")] -use pharmsol_dsl::ModelKind; -#[cfg(feature = "dsl-aot")] -use pharmsol_dsl::{analyze_module, compile_analyzed_model, parse_module, ExecutionModel}; -use pharmsol_dsl::{AnalysisError, CompileError, Diagnostic, DiagnosticReport, ParseError}; - -/// ABI version for native AoT artifacts produced by this crate. -pub const AOT_API_VERSION: u32 = 2; - -#[cfg(feature = "dsl-aot")] -/// Selects the compilation target for a native ahead-of-time artifact. -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub enum NativeAotTarget { - /// Compile for the current host toolchain target. - #[default] - Host, - /// Compile for an explicit Rust target triple. - Triple(String), -} - -#[cfg(feature = "dsl-aot")] -impl NativeAotTarget { - /// Create a target selector for an explicit Rust target triple. - pub fn triple(target: impl Into) -> Self { - Self::Triple(target.into()) - } - - fn cargo_target(&self) -> Option<&str> { - match self { - Self::Host => None, - Self::Triple(target) => Some(target.as_str()), - } - } -} - -#[cfg(feature = "dsl-aot")] -/// Options that control native ahead-of-time artifact export. -/// -/// AoT export writes a small template crate under [`template_root`](Self::template_root), -/// builds a native shared library, and then copies the resulting artifact to -/// [`output`](Self::output) or a generated default path. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct NativeAotCompileOptions { - /// Target triple selection for the emitted artifact. - pub target: NativeAotTarget, - /// Optional final artifact location. - pub output: Option, - /// Working directory used for the temporary template crate and build output. - pub template_root: PathBuf, -} - -#[cfg(feature = "dsl-aot")] -impl NativeAotCompileOptions { - /// Create AoT options rooted at a template build directory. - pub fn new(template_root: PathBuf) -> Self { - Self { - target: NativeAotTarget::Host, - output: None, - template_root, - } - } - - /// Set the final artifact output path. - pub fn with_output(mut self, output: PathBuf) -> Self { - self.output = Some(output); - self - } - - /// Set the compilation target triple. - pub fn with_target(mut self, target: NativeAotTarget) -> Self { - self.target = target; - self - } -} - -/// Error produced while exporting, reading, or loading a native AoT artifact. -#[derive(Error)] -pub enum AotError { - #[error(transparent)] - Io(#[from] io::Error), - #[error(transparent)] - Json(#[from] serde_json::Error), - #[error("failed to parse DSL source: {0}")] - Parse(#[source] ParseError), - #[error("failed to analyze DSL source: {0}")] - Analysis(#[source] AnalysisError), - #[error("failed to compile DSL model: {0}")] - Compile(#[source] CompileError), - #[error("{0}")] - ModelSelection(String), - #[error("AoT artifact API version mismatch: expected {expected}, found {found}")] - ApiVersionMismatch { expected: u32, found: u32 }, - #[error("missing required AoT symbol `{0}`")] - MissingSymbol(&'static str), - #[error("failed to emit AoT library source: {0}")] - Emit(String), - #[error("failed to load AoT artifact: {0}")] - Load(String), -} - -impl AotError { - pub fn diagnostic(&self) -> Option<&Diagnostic> { - match self { - Self::Parse(error) => Some(error.diagnostic()), - Self::Analysis(error) => Some(error.diagnostic()), - Self::Compile(error) => Some(error.diagnostic()), - _ => None, - } - } - - pub fn render_diagnostic(&self, src: &str) -> Option { - self.diagnostic().map(|diagnostic| diagnostic.render(src)) - } - - pub fn diagnostic_report(&self, source_name: impl Into) -> Option { - let source_name = source_name.into(); - match self { - Self::Parse(error) => Some(error.diagnostic_report(source_name)), - Self::Analysis(error) => Some(error.diagnostic_report(source_name)), - Self::Compile(error) => Some(error.diagnostic_report(source_name)), - _ => None, - } - } -} - -impl fmt::Debug for AotError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Parse(error) => fmt::Display::fmt(error, f), - Self::Analysis(error) => fmt::Display::fmt(error, f), - Self::Compile(error) => fmt::Display::fmt(error, f), - _ => fmt::Display::fmt(self, f), - } - } -} - -#[cfg(feature = "dsl-aot")] -/// Parse DSL source, lower one selected model, and export a native AoT artifact. -/// -/// Use this when you want a reusable native artifact that can be loaded later -/// with [`load_aot_model`] or [`crate::dsl::load_runtime_artifact`]. -/// -/// This function requires the `dsl-aot` feature. Loading the resulting artifact -/// later requires `dsl-aot-load`. -/// -/// ```rust,no_run -/// use std::path::PathBuf; -/// -/// use pharmsol::dsl::{compile_module_source_to_aot, load_aot_model, NativeAotCompileOptions}; -/// -/// let source = r#" -/// name = bimodal_ke -/// kind = ode -/// -/// params = ke, v -/// states = central -/// outputs = cp -/// -/// infusion(iv) -> central -/// -/// dx(central) = -ke * central -/// out(cp) = central / v -/// "#; -/// -/// let artifact = compile_module_source_to_aot( -/// source, -/// Some("bimodal_ke"), -/// NativeAotCompileOptions::new(PathBuf::from("target/doc-aot-build")), -/// |_, _| {}, -/// )?; -/// let loaded = load_aot_model(&artifact)?; -/// # let _ = loaded; -/// # Ok::<(), Box>(()) -/// ``` -pub fn compile_module_source_to_aot( - source: &str, - model_name: Option<&str>, - options: NativeAotCompileOptions, - event_callback: impl Fn(String, String) + Send + Sync + 'static, -) -> Result { - let parsed = - parse_module(source).map_err(|error| AotError::Parse(error.with_source(source)))?; - let analyzed = - analyze_module(&parsed).map_err(|error| AotError::Analysis(error.with_source(source)))?; - - let model = match model_name { - Some(name) => analyzed - .models - .iter() - .find(|model| model.name == name) - .ok_or_else(|| { - AotError::ModelSelection(format!("model `{name}` not found in module")) - })?, - None if analyzed.models.len() == 1 => &analyzed.models[0], - None => { - return Err(AotError::ModelSelection( - "module contains multiple models; pass an explicit model name".to_string(), - )) - } - }; - - let execution = compile_analyzed_model(model) - .map_err(|error| AotError::Compile(error.with_source(source)))?; - export_execution_model_to_aot(&execution, options, event_callback) -} - -#[cfg(feature = "dsl-aot")] -/// Export a compiled execution model as a native AoT artifact. -/// -/// Use this lower-level entrypoint when you already own the frontend pipeline -/// and only need artifact generation. -pub fn export_execution_model_to_aot( - model: &ExecutionModel, - options: NativeAotCompileOptions, - event_callback: impl Fn(String, String) + Send + Sync + 'static, -) -> Result { - let event_callback = Arc::new(event_callback); - let NativeAotCompileOptions { - target, - output, - template_root, - } = options; - let cargo_target = target.cargo_target(); - let template_dir = create_cargo_template(template_root.clone(), &aot_template_manifest())?; - let source = emit_rust_backend_source( - model, - RustBackendFlavor::NativeAot { - api_version: AOT_API_VERSION, - }, - ) - .map_err(AotError::Emit)?; - write_template_source(&template_dir, &source)?; - - let dylib_name = native_cdylib_filename_for_target("model_lib", cargo_target); - let dylib_path = match cargo_target { - Some(target) => build_cargo_template( - template_dir, - event_callback.clone(), - "native-aot", - model.name.clone(), - Some(target), - &[target, "release", dylib_name.as_str()], - )?, - None => build_cargo_template( - template_dir, - event_callback.clone(), - "native-aot", - model.name.clone(), - None, - &["release", dylib_name.as_str()], - )?, - }; - - let output_path = output.unwrap_or_else(|| default_output_path(&template_root, &target)); - fs::copy(&dylib_path, &output_path)?; - event_callback( - "finished".into(), - format!( - "Compiled native-aot model `{}` -> {}", - model.name, - output_path.display() - ), - ); - Ok(output_path) -} - -#[cfg(feature = "dsl-aot-load")] -/// Read only the metadata from a native AoT artifact. -/// -/// This is useful when you need to inspect model identity, routes, outputs, or -/// buffer sizes without loading the executable functions. -pub fn read_aot_model_info(path: impl AsRef) -> Result { - let library = unsafe { Library::new(path.as_ref()) } - .map_err(|error| AotError::Load(error.to_string()))?; - let info = unsafe { read_model_info_from_library(&library)? }; - Ok(info) -} - -#[cfg(feature = "dsl-aot-load")] -/// Load a native AoT artifact into the native execution runtime. -pub fn load_aot_model(path: impl AsRef) -> Result { - let path = path.as_ref(); - let library = - unsafe { Library::new(path) }.map_err(|error| AotError::Load(error.to_string()))?; - - unsafe { ensure_api_version(&library)? }; - let info = unsafe { read_model_info_from_library(&library)? }; - let model_name = info.name.clone(); - let artifact = unsafe { - NativeExecutionArtifact::from_library( - model_name, - load_optional_function(&library, DERIVE_SYMBOL), - load_optional_function(&library, DYNAMICS_SYMBOL), - load_required_function(&library, OUTPUTS_SYMBOL)?, - load_optional_function(&library, INIT_SYMBOL), - load_optional_function(&library, DRIFT_SYMBOL), - load_optional_function(&library, DIFFUSION_SYMBOL), - load_optional_function(&library, ROUTE_LAG_SYMBOL), - load_optional_function(&library, ROUTE_BIOAVAILABILITY_SYMBOL), - library, - ) - }; - - Ok(match info.kind { - ModelKind::Ode => CompiledNativeModel::Ode( - super::NativeOdeModel::new(info, artifact) - .map_err(|error| AotError::Load(error.to_string()))?, - ), - ModelKind::Analytical => CompiledNativeModel::Analytical( - super::NativeAnalyticalModel::new(info, artifact) - .map_err(|error| AotError::Load(error.to_string()))?, - ), - ModelKind::Sde => CompiledNativeModel::Sde( - super::NativeSdeModel::new(info, artifact) - .map_err(|error| AotError::Load(error.to_string()))?, - ), - }) -} - -#[cfg(feature = "dsl-aot")] -fn default_output_path(template_root: &Path, target: &NativeAotTarget) -> PathBuf { - let random_suffix: String = rand::rng() - .sample_iter(&Alphanumeric) - .take(5) - .map(char::from) - .collect(); - let target_label = match target { - NativeAotTarget::Host => default_target_label(), - NativeAotTarget::Triple(target) => sanitize_target_label(target), - }; - template_root.join(format!("model_{}_{}.pkm", target_label, random_suffix)) -} - -#[cfg(feature = "dsl-aot")] -fn default_target_label() -> String { - sanitize_target_label(&format!( - "{}-{}", - std::env::consts::ARCH, - std::env::consts::OS - )) -} - -#[cfg(feature = "dsl-aot")] -fn sanitize_target_label(target: &str) -> String { - target - .chars() - .map(|ch| if ch.is_ascii_alphanumeric() { ch } else { '_' }) - .collect() -} - -#[cfg(feature = "dsl-aot")] -fn aot_template_manifest() -> String { - r#" - [package] - name = "model_lib" - version = "0.1.0" - edition = "2021" - - [lib] - crate-type = ["cdylib"] - - [workspace] - "# - .to_string() -} - -#[cfg(feature = "dsl-aot-load")] -unsafe fn ensure_api_version(library: &Library) -> Result<(), AotError> { - let symbol: Symbol u32> = library - .get(API_VERSION_SYMBOL.as_bytes()) - .map_err(|_| AotError::MissingSymbol(API_VERSION_SYMBOL))?; - let found = symbol(); - if found != AOT_API_VERSION { - return Err(AotError::ApiVersionMismatch { - expected: AOT_API_VERSION, - found, - }); - } - Ok(()) -} - -#[cfg(feature = "dsl-aot-load")] -unsafe fn read_model_info_from_library(library: &Library) -> Result { - ensure_api_version(library)?; - let ptr_symbol: Symbol *const u8> = library - .get(MODEL_INFO_JSON_PTR_SYMBOL.as_bytes()) - .map_err(|_| AotError::MissingSymbol(MODEL_INFO_JSON_PTR_SYMBOL))?; - let len_symbol: Symbol usize> = library - .get(MODEL_INFO_JSON_LEN_SYMBOL.as_bytes()) - .map_err(|_| AotError::MissingSymbol(MODEL_INFO_JSON_LEN_SYMBOL))?; - - let ptr = ptr_symbol(); - let len = len_symbol(); - let bytes = std::slice::from_raw_parts(ptr, len); - let envelope = decode_compiled_model_info(bytes)?; - if envelope.abi_version != AOT_API_VERSION { - return Err(AotError::ApiVersionMismatch { - expected: AOT_API_VERSION, - found: envelope.abi_version, - }); - } - Ok(envelope.model) -} - -#[cfg(feature = "dsl-aot-load")] -unsafe fn load_required_function( - library: &Library, - name: &'static str, -) -> Result { - let symbol: Symbol = library - .get(name.as_bytes()) - .map_err(|_| AotError::MissingSymbol(name))?; - Ok(*symbol) -} - -#[cfg(feature = "dsl-aot-load")] -unsafe fn load_optional_function( - library: &Library, - name: &'static str, -) -> Option { - library - .get::(name.as_bytes()) - .ok() - .map(|symbol| *symbol) -} - -#[cfg(all( - test, - feature = "dsl-aot", - feature = "dsl-aot-load", - feature = "dsl-jit" -))] -mod tests { - use super::*; - use crate::dsl::compile_ode_model_to_jit; - use crate::test_fixtures::STRUCTURED_BLOCK_CORPUS; - use crate::{Parameters, SubjectBuilderExt}; - use approx::assert_relative_eq; - use pharmsol_dsl::{DiagnosticPhase, DSL_ANALYSIS_GENERIC}; - use std::sync::{Arc, Mutex}; - use tempfile::tempdir; - - const CROSS_TARGET_SMOKE_ENV: &str = "PHARMSOL_NATIVE_AOT_SMOKE_TARGET"; - - enum CrossTargetSmokeDecision { - Run(String), - Skip(String), - } - - fn load_corpus_model(name: &str) -> ExecutionModel { - let source = STRUCTURED_BLOCK_CORPUS; - let parsed = pharmsol_dsl::parse_module(source).expect("parse corpus module"); - let analyzed = pharmsol_dsl::analyze_module(&parsed).expect("analyze corpus module"); - let model = analyzed - .models - .iter() - .find(|model| model.name == name) - .expect("model in corpus module"); - pharmsol_dsl::compile_analyzed_model(model).expect("lower corpus model") - } - - fn resolve_cross_target_smoke_target() -> Result { - let requested_target = std::env::var(CROSS_TARGET_SMOKE_ENV) - .ok() - .map(|value| value.trim().to_string()) - .filter(|value| !value.is_empty()); - let host_target = rustc_host_target() - .map_err(|error| format!("failed to detect the Rust host target: {error}"))?; - - let installed_targets = match rustup_installed_targets() { - Ok(targets) => targets, - Err(error) if requested_target.is_none() => { - return Ok(CrossTargetSmokeDecision::Skip(format!( - "rustup target discovery is unavailable: {error}" - ))) - } - Err(error) => { - return Err(format!( - "{CROSS_TARGET_SMOKE_ENV} is set, but installed targets could not be queried: {error}" - )) - } - }; - - if let Some(target) = requested_target { - if target == host_target { - return Err(format!( - "{CROSS_TARGET_SMOKE_ENV} must name a non-host native target, but `{target}` matches the host" - )); - } - if !is_native_target_triple(&target) { - return Err(format!( - "{CROSS_TARGET_SMOKE_ENV} must name a native target triple, but `{target}` is not supported for native AoT" - )); - } - if !installed_targets - .iter() - .any(|installed| installed == &target) - { - return Err(format!( - "{CROSS_TARGET_SMOKE_ENV} requested `{target}`, but it is not installed. Run `rustup target add {target}` first." - )); - } - return Ok(CrossTargetSmokeDecision::Run(target)); - } - - if let Some(target) = - auto_detect_cross_target_smoke_target(&host_target, &installed_targets) - { - return Ok(CrossTargetSmokeDecision::Run(target)); - } - - Ok(CrossTargetSmokeDecision::Skip(format!( - "no supported non-host native target is installed; set {CROSS_TARGET_SMOKE_ENV} after installing a target and linker" - ))) - } - - fn auto_detect_cross_target_smoke_target( - host_target: &str, - installed_targets: &[String], - ) -> Option { - let preferred = match host_target { - "aarch64-apple-darwin" => &["x86_64-apple-darwin"][..], - "x86_64-apple-darwin" => &["aarch64-apple-darwin"][..], - _ => &[][..], - }; - - preferred - .iter() - .find(|candidate| { - installed_targets - .iter() - .any(|installed| installed == *candidate) - }) - .map(|candidate| (*candidate).to_string()) - } - - fn is_native_target_triple(target: &str) -> bool { - !target.starts_with("wasm32-") && !target.starts_with("wasm64-") - } - - fn render_captured_events(events: &Arc>>) -> String { - let events = events - .lock() - .expect("cross-target smoke event log mutex poisoned"); - if events.is_empty() { - return "".to_string(); - } - - events - .iter() - .map(|(kind, message)| format!("[{kind}] {}", message.trim_end())) - .collect::>() - .join("\n") - } - - #[test] - fn aot_ode_artifact_matches_jit_predictions() { - let model = load_corpus_model("one_cmt_oral_iv"); - let work_dir = tempdir().expect("tempdir"); - let output_path = work_dir.path().join("one_cmt_oral_iv.pkm"); - - let jit = compile_ode_model_to_jit(&model).expect("compile jit model"); - export_execution_model_to_aot( - &model, - NativeAotCompileOptions::new(work_dir.path().join("build")) - .with_output(output_path.clone()), - |_, _| {}, - ) - .expect("export aot model"); - - let loaded = load_aot_model(&output_path).expect("load aot model"); - let aot = match loaded { - CompiledNativeModel::Ode(model) => model, - other => panic!("expected ode model, got {other:?}"), - }; - - let oral = jit - .info() - .routes - .iter() - .find(|route| route.name == "oral") - .map(|route| route.index) - .expect("jit oral route"); - let iv = jit - .info() - .routes - .iter() - .find(|route| route.name == "iv") - .map(|route| route.index) - .expect("jit iv route"); - let cp = jit - .info() - .outputs - .iter() - .find(|output| output.name == "cp") - .map(|output| output.index) - .expect("jit cp output"); - assert_eq!( - aot.info() - .routes - .iter() - .find(|route| route.name == "oral") - .map(|route| route.index), - Some(oral) - ); - assert_eq!( - aot.info() - .routes - .iter() - .find(|route| route.name == "iv") - .map(|route| route.index), - Some(iv) - ); - assert_eq!( - aot.info() - .outputs - .iter() - .find(|output| output.name == "cp") - .map(|output| output.index), - Some(cp) - ); - - let subject = crate::Subject::builder("ode") - .covariate("wt", 0.0, 70.0) - .bolus(0.0, 120.0, "oral") - .infusion(6.0, 60.0, "iv", 2.0) - .missing_observation(0.5, "cp") - .missing_observation(1.0, "cp") - .missing_observation(2.0, "cp") - .missing_observation(6.0, "cp") - .missing_observation(7.0, "cp") - .missing_observation(9.0, "cp") - .build(); - - let support = Parameters::with_model( - &crate::dsl::CompiledRuntimeModel::Ode(jit.clone()), - [ - ("ka", 1.2), - ("cl", 5.0), - ("v", 40.0), - ("tlag", 0.5), - ("f_oral", 0.8), - ], - ) - .expect("valid named parameters"); - let jit_predictions = jit - .estimate_predictions(&subject, &support) - .expect("jit predictions"); - let aot_predictions = aot - .estimate_predictions(&subject, &support) - .expect("aot predictions"); - - for (jit_pred, aot_pred) in jit_predictions - .predictions() - .iter() - .zip(aot_predictions.predictions()) - { - assert_relative_eq!( - jit_pred.prediction(), - aot_pred.prediction(), - max_relative = 1e-4 - ); - } - - let info = read_aot_model_info(&output_path).expect("aot model info"); - assert_eq!(info.name, "one_cmt_oral_iv"); - assert_eq!(info.kind, ModelKind::Ode); - assert_eq!(info.parameters, vec!["ka", "cl", "v", "tlag", "f_oral"]); - } - - #[test] - fn native_cdylib_filename_tracks_requested_target() { - assert_eq!( - native_cdylib_filename_for_target("model_lib", Some("x86_64-pc-windows-msvc")), - "model_lib.dll" - ); - assert_eq!( - native_cdylib_filename_for_target("model_lib", Some("aarch64-apple-darwin")), - "libmodel_lib.dylib" - ); - assert_eq!( - native_cdylib_filename_for_target("model_lib", Some("x86_64-unknown-linux-gnu")), - "libmodel_lib.so" - ); - } - - #[test] - fn default_output_path_uses_requested_target_label() { - let work_dir = tempdir().expect("tempdir"); - let output = default_output_path( - work_dir.path(), - &NativeAotTarget::triple("x86_64-pc-windows-msvc"), - ); - let file_name = output - .file_name() - .expect("output file name") - .to_string_lossy(); - assert!(file_name.starts_with("model_x86_64_pc_windows_msvc_")); - assert!(file_name.ends_with(".pkm")); - } - - #[test] - fn native_aot_compile_options_default_to_host_target() { - let work_dir = tempdir().expect("tempdir"); - let options = NativeAotCompileOptions::new(work_dir.path().join("build")); - assert_eq!(options.target, NativeAotTarget::Host); - assert_eq!(options.output, None); - } - - #[test] - fn native_aot_cross_target_smoke_builds_when_supported() { - let target = match resolve_cross_target_smoke_target() { - Ok(CrossTargetSmokeDecision::Run(target)) => target, - Ok(CrossTargetSmokeDecision::Skip(reason)) => { - eprintln!("skipping Native AoT cross-target smoke test: {reason}"); - return; - } - Err(error) => panic!("invalid cross-target smoke configuration: {error}"), - }; - - let model = load_corpus_model("one_cmt_oral_iv"); - let work_dir = tempdir().expect("tempdir"); - let output_path = work_dir.path().join(format!( - "one_cmt_oral_iv_{}.pkm", - sanitize_target_label(&target) - )); - let events = Arc::new(Mutex::new(Vec::<(String, String)>::new())); - let captured_events = Arc::clone(&events); - - let result = export_execution_model_to_aot( - &model, - NativeAotCompileOptions::new(work_dir.path().join("cross-target-build")) - .with_target(NativeAotTarget::triple(target.clone())) - .with_output(output_path.clone()), - move |kind, message| { - captured_events - .lock() - .expect("cross-target smoke event log mutex poisoned") - .push((kind, message)); - }, - ); - - match result { - Ok(path) => { - assert_eq!(path, output_path); - assert!(path.exists()); - } - Err(error) => panic!( - "Native AoT cross-target smoke build failed for `{target}`: {error}\n{}", - render_captured_events(&events) - ), - } - } - - #[test] - fn aot_compile_preserves_analysis_diagnostic_structure() { - let source = r#" -model broken { - kind ode - states { central } - dynamics { - ddt(central) = rate(oral) - } - outputs { - cp = central - } -} -"#; - let work_dir = tempdir().expect("tempdir"); - let error = compile_module_source_to_aot( - source, - None, - NativeAotCompileOptions::new(work_dir.path().join("build")), - |_, _| {}, - ) - .expect_err("invalid DSL should fail before AoT compilation"); - - let diagnostic = error.diagnostic().expect("AoT should expose diagnostic"); - assert_eq!(diagnostic.phase, DiagnosticPhase::Analysis); - assert_eq!(diagnostic.code, DSL_ANALYSIS_GENERIC); - assert!(diagnostic.message.contains("unknown route `oral`")); - let rendered = error - .render_diagnostic(source) - .expect("rendered diagnostic"); - assert!(rendered.contains("error[DSL2000]"), "{}", rendered); - assert!(rendered.contains("unknown route `oral`"), "{}", rendered); - let debugged = format!("{error:?}"); - assert!(debugged.contains("error[DSL2000]"), "{}", debugged); - assert!(debugged.contains("unknown route `oral`"), "{}", debugged); - let report = error - .diagnostic_report("inline.dsl") - .expect("diagnostic report"); - assert_eq!(report.source.name, "inline.dsl"); - assert_eq!(report.diagnostics[0].code, "DSL2000"); - assert!(!report.diagnostics[0].labels.is_empty()); - } - - #[test] - fn aot_compile_preserves_analysis_suggestions() { - let source = r#" -model broken { - kind ode - states { central } - routes { oral -> central } - dynamics { - ddt(central) = rate(orla) - } - outputs { - cp = central - } -} -"#; - let work_dir = tempdir().expect("tempdir"); - let error = compile_module_source_to_aot( - source, - None, - NativeAotCompileOptions::new(work_dir.path().join("build-suggestions")), - |_, _| {}, - ) - .expect_err("invalid DSL should fail before AoT compilation"); - - let diagnostic = error.diagnostic().expect("AoT should expose diagnostic"); - assert!(diagnostic - .suggestions - .iter() - .any(|suggestion| suggestion.message.contains("did you mean `oral`?"))); - - let rendered = error - .render_diagnostic(source) - .expect("rendered diagnostic"); - assert!( - rendered.contains("suggestion: did you mean `oral`?"), - "{}", - rendered - ); - } -} diff --git a/src/dsl/compiled_backend_abi.rs b/src/dsl/compiled_backend_abi.rs deleted file mode 100644 index 388a0e77..00000000 --- a/src/dsl/compiled_backend_abi.rs +++ /dev/null @@ -1,313 +0,0 @@ -use serde::{Deserialize, Serialize}; - -use super::model_info::NativeModelInfo; -use pharmsol_dsl::execution::{ExecutionModel, ModelFunctionKind}; - -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const API_VERSION_SYMBOL: &str = "pharmsol_dsl_api_version"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const MODEL_INFO_JSON_PTR_SYMBOL: &str = "pharmsol_dsl_model_info_json_ptr"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const MODEL_INFO_JSON_LEN_SYMBOL: &str = "pharmsol_dsl_model_info_json_len"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const DERIVE_SYMBOL: &str = "pharmsol_dsl_kernel_derive"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const DYNAMICS_SYMBOL: &str = "pharmsol_dsl_kernel_dynamics"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const OUTPUTS_SYMBOL: &str = "pharmsol_dsl_kernel_outputs"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const INIT_SYMBOL: &str = "pharmsol_dsl_kernel_init"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const DRIFT_SYMBOL: &str = "pharmsol_dsl_kernel_drift"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const DIFFUSION_SYMBOL: &str = "pharmsol_dsl_kernel_diffusion"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const ROUTE_LAG_SYMBOL: &str = "pharmsol_dsl_kernel_route_lag"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const ROUTE_BIOAVAILABILITY_SYMBOL: &str = "pharmsol_dsl_kernel_route_bioavailability"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const ALLOC_F64_BUFFER_SYMBOL: &str = "pharmsol_dsl_alloc_f64_buffer"; -#[cfg(any(test, feature = "dsl-aot", feature = "dsl-aot-load"))] -pub const FREE_F64_BUFFER_SYMBOL: &str = "pharmsol_dsl_free_f64_buffer"; - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompiledFunctionAvailability { - pub derive: bool, - pub dynamics: bool, - pub outputs: bool, - pub init: bool, - pub drift: bool, - pub diffusion: bool, - pub route_lag: bool, - pub route_bioavailability: bool, -} - -impl CompiledFunctionAvailability { - pub fn from_execution_model(model: &ExecutionModel) -> Self { - let mut availability = Self::default(); - for function in &model.functions { - match function.kind { - ModelFunctionKind::Derive => availability.derive = true, - ModelFunctionKind::Dynamics => availability.dynamics = true, - ModelFunctionKind::Outputs => availability.outputs = true, - ModelFunctionKind::Init => availability.init = true, - ModelFunctionKind::Drift => availability.drift = true, - ModelFunctionKind::Diffusion => availability.diffusion = true, - ModelFunctionKind::RouteLag => availability.route_lag = true, - ModelFunctionKind::RouteBioavailability => { - availability.route_bioavailability = true - } - ModelFunctionKind::Analytical => {} - } - } - availability - } - - pub fn has(self, role: ModelFunctionKind) -> bool { - match role { - ModelFunctionKind::Derive => self.derive, - ModelFunctionKind::Dynamics => self.dynamics, - ModelFunctionKind::Outputs => self.outputs, - ModelFunctionKind::Init => self.init, - ModelFunctionKind::Drift => self.drift, - ModelFunctionKind::Diffusion => self.diffusion, - ModelFunctionKind::RouteLag => self.route_lag, - ModelFunctionKind::RouteBioavailability => self.route_bioavailability, - ModelFunctionKind::Analytical => false, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompiledModelInfoEnvelope { - pub abi_version: u32, - pub model: NativeModelInfo, - pub functions: CompiledFunctionAvailability, -} - -#[cfg(feature = "dsl-aot")] -pub fn compiled_model_info_envelope( - model: &ExecutionModel, - abi_version: u32, -) -> CompiledModelInfoEnvelope { - CompiledModelInfoEnvelope { - abi_version, - model: NativeModelInfo::from_execution_model(model), - functions: CompiledFunctionAvailability::from_execution_model(model), - } -} - -#[cfg(feature = "dsl-aot")] -pub fn encode_compiled_model_info( - model: &ExecutionModel, - abi_version: u32, -) -> Result { - serde_json::to_string(&compiled_model_info_envelope(model, abi_version)) -} - -#[cfg(any(test, feature = "dsl-aot-load"))] -pub fn decode_compiled_model_info( - bytes: &[u8], -) -> Result { - serde_json::from_slice(bytes) -} - -#[cfg(feature = "dsl-aot")] -pub fn compiled_function_symbol(role: ModelFunctionKind) -> Option<&'static str> { - match role { - ModelFunctionKind::Derive => Some(DERIVE_SYMBOL), - ModelFunctionKind::Dynamics => Some(DYNAMICS_SYMBOL), - ModelFunctionKind::Outputs => Some(OUTPUTS_SYMBOL), - ModelFunctionKind::Init => Some(INIT_SYMBOL), - ModelFunctionKind::Drift => Some(DRIFT_SYMBOL), - ModelFunctionKind::Diffusion => Some(DIFFUSION_SYMBOL), - ModelFunctionKind::RouteLag => Some(ROUTE_LAG_SYMBOL), - ModelFunctionKind::RouteBioavailability => Some(ROUTE_BIOAVAILABILITY_SYMBOL), - ModelFunctionKind::Analytical => None, - } -} - -#[cfg(test)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum OutputBufferBinding { - States, - Derived, - Scratch, -} - -#[cfg(test)] -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct OutputBufferPlan { - pub binding: OutputBufferBinding, - pub len: usize, - pub zero_before_call: bool, -} - -#[cfg(test)] -pub fn output_buffer_plan( - info: &NativeModelInfo, - role: ModelFunctionKind, - aliases_states: bool, - aliases_derived: bool, -) -> OutputBufferPlan { - let binding = if aliases_states { - OutputBufferBinding::States - } else if aliases_derived { - OutputBufferBinding::Derived - } else { - OutputBufferBinding::Scratch - }; - - OutputBufferPlan { - binding, - len: function_output_len(info, role), - zero_before_call: matches!(binding, OutputBufferBinding::Scratch), - } -} - -#[cfg(test)] -fn function_output_len(info: &NativeModelInfo, role: ModelFunctionKind) -> usize { - match role { - ModelFunctionKind::Derive => info.derived_len, - ModelFunctionKind::Dynamics - | ModelFunctionKind::Init - | ModelFunctionKind::Drift - | ModelFunctionKind::Diffusion => info.state_len, - ModelFunctionKind::Outputs => info.output_len, - ModelFunctionKind::RouteLag | ModelFunctionKind::RouteBioavailability => info.route_len, - ModelFunctionKind::Analytical => 0, - } -} - -#[cfg(test)] -mod tests { - use super::super::model_info::{ - NativeCovariateInfo, NativeOutputInfo, NativeRouteInfo, NativeStateInfo, - }; - use super::*; - use pharmsol_dsl::{ModelKind, RouteKind}; - - #[test] - fn compiled_backend_symbol_names_are_frozen() { - assert_eq!(API_VERSION_SYMBOL, "pharmsol_dsl_api_version"); - assert_eq!( - MODEL_INFO_JSON_PTR_SYMBOL, - "pharmsol_dsl_model_info_json_ptr" - ); - assert_eq!( - MODEL_INFO_JSON_LEN_SYMBOL, - "pharmsol_dsl_model_info_json_len" - ); - assert_eq!(DERIVE_SYMBOL, "pharmsol_dsl_kernel_derive"); - assert_eq!(DYNAMICS_SYMBOL, "pharmsol_dsl_kernel_dynamics"); - assert_eq!(OUTPUTS_SYMBOL, "pharmsol_dsl_kernel_outputs"); - assert_eq!(INIT_SYMBOL, "pharmsol_dsl_kernel_init"); - assert_eq!(DRIFT_SYMBOL, "pharmsol_dsl_kernel_drift"); - assert_eq!(DIFFUSION_SYMBOL, "pharmsol_dsl_kernel_diffusion"); - assert_eq!(ROUTE_LAG_SYMBOL, "pharmsol_dsl_kernel_route_lag"); - assert_eq!( - ROUTE_BIOAVAILABILITY_SYMBOL, - "pharmsol_dsl_kernel_route_bioavailability" - ); - assert_eq!(ALLOC_F64_BUFFER_SYMBOL, "pharmsol_dsl_alloc_f64_buffer"); - assert_eq!(FREE_F64_BUFFER_SYMBOL, "pharmsol_dsl_free_f64_buffer"); - } - - #[test] - fn compiled_model_info_round_trips_function_availability_and_dimensions() { - let envelope = CompiledModelInfoEnvelope { - abi_version: 7, - model: NativeModelInfo { - name: "example".to_string(), - kind: ModelKind::Ode, - parameters: vec!["ke".to_string(), "v".to_string()], - derived: vec!["ke_i".to_string(), "v_i".to_string(), "cl_i".to_string()], - covariates: vec![NativeCovariateInfo { - name: "wt".to_string(), - index: 0, - interpolation: None, - }], - states: vec![ - NativeStateInfo { - name: "depot".to_string(), - offset: 0, - }, - NativeStateInfo { - name: "central".to_string(), - offset: 1, - }, - ], - routes: vec![NativeRouteInfo { - name: "iv".to_string(), - declaration_index: 0, - index: 0, - kind: Some(RouteKind::Infusion), - destination_offset: 1, - destination_name: "central".to_string(), - has_lag: false, - has_bioavailability: false, - inject_input_to_destination: true, - }], - outputs: vec![NativeOutputInfo { - name: "cp".to_string(), - index: 0, - }], - state_len: 2, - derived_len: 3, - output_len: 1, - route_len: 1, - analytical: None, - particles: Some(32), - }, - functions: CompiledFunctionAvailability { - derive: true, - dynamics: true, - outputs: true, - init: true, - drift: false, - diffusion: false, - route_lag: true, - route_bioavailability: false, - }, - }; - - let json = serde_json::to_vec(&envelope).expect("serialize envelope"); - let decoded = decode_compiled_model_info(&json).expect("decode envelope"); - assert_eq!(decoded, envelope); - } - - #[test] - fn output_buffer_plan_tracks_aliasing_and_zeroing_rules() { - let info = NativeModelInfo { - name: "example".to_string(), - kind: ModelKind::Ode, - parameters: vec![], - derived: vec!["ke_i".to_string(), "v_i".to_string(), "cl_i".to_string()], - covariates: vec![], - states: vec![], - routes: vec![], - outputs: vec![], - state_len: 2, - derived_len: 3, - output_len: 4, - route_len: 1, - analytical: None, - particles: None, - }; - - let scratch = output_buffer_plan(&info, ModelFunctionKind::Diffusion, false, false); - assert_eq!(scratch.binding, OutputBufferBinding::Scratch); - assert_eq!(scratch.len, 2); - assert!(scratch.zero_before_call); - - let states = output_buffer_plan(&info, ModelFunctionKind::Dynamics, true, false); - assert_eq!(states.binding, OutputBufferBinding::States); - assert_eq!(states.len, 2); - assert!(!states.zero_before_call); - - let derived = output_buffer_plan(&info, ModelFunctionKind::Derive, false, true); - assert_eq!(derived.binding, OutputBufferBinding::Derived); - assert_eq!(derived.len, 3); - assert!(!derived.zero_before_call); - } -} diff --git a/src/dsl/mod.rs b/src/dsl/mod.rs index 2ba8c494..b0a93d4b 100644 --- a/src/dsl/mod.rs +++ b/src/dsl/mod.rs @@ -19,8 +19,6 @@ //! analyzed models into the ready-to-run form used by the runtime backends. //! - [`compile_module_source_to_runtime`] and [`compile_execution_model_to_runtime`] //! for the one-stop compile-and-run path. -//! - [`load_runtime_artifact`] and [`load_aot_model`] for loading saved -//! artifacts back into a model you can execute. //! //! Common workflow choices: //! @@ -28,8 +26,6 @@ //! you need diagnostics, authoring tools, or your own backend. //! - In-process execution: compile straight to [`RuntimeCompilationTarget`] and //! keep everything inside the current process. -//! - Native artifact shipping: export a native AoT artifact, then load it later -//! on a compatible host. //! //! Feature map: //! @@ -39,10 +35,6 @@ //! [`compile_module_source_to_runtime`] with //! [`RuntimeCompilationTarget::Jit`], plus the lower-level JIT compile //! entrypoints. -//! - `dsl-aot`: enables native ahead-of-time artifact export through -//! [`compile_module_source_to_aot`] and [`export_execution_model_to_aot`]. -//! - `dsl-aot-load`: enables native AoT artifact loading through -//! [`load_aot_model`] and [`read_aot_model_info`]. //! //! Smallest compile-to-runtime example: //! @@ -80,32 +72,14 @@ //! `pharmsol-dsl`. For a complete runtime path inside the main crate, stay in //! [`pharmsol::dsl`](self). -#[cfg(any(feature = "dsl-aot", feature = "dsl-aot-load"))] -mod aot; -mod compiled_backend_abi; #[cfg(feature = "dsl-jit")] mod jit; mod model_info; -#[cfg(any(feature = "dsl-jit", feature = "dsl-aot-load"))] +#[cfg(feature = "dsl-jit")] mod native; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] mod runtime; -#[cfg(feature = "dsl-aot")] -mod rust_backend; -#[cfg(feature = "dsl-aot")] -pub use aot::{ - compile_module_source_to_aot, export_execution_model_to_aot, AotError, NativeAotCompileOptions, - NativeAotTarget, AOT_API_VERSION, -}; -#[cfg(feature = "dsl-aot-load")] -pub use aot::{load_aot_model, read_aot_model_info}; -#[cfg(all(not(feature = "dsl-aot"), feature = "dsl-aot-load"))] -pub use aot::{AotError, AOT_API_VERSION}; -pub use compiled_backend_abi::{CompiledFunctionAvailability, CompiledModelInfoEnvelope}; #[cfg(feature = "dsl-jit")] pub use jit::{ compile_analytical_model_to_jit, compile_execution_artifact, compile_execution_model_to_jit, @@ -113,19 +87,16 @@ pub use jit::{ JitCompileError, JitExecutionArtifact, JitOdeModel, JitSdeModel, }; pub use model_info::{NativeCovariateInfo, NativeModelInfo, NativeOutputInfo, NativeRouteInfo}; -#[cfg(any(feature = "dsl-jit", feature = "dsl-aot-load"))] +#[cfg(feature = "dsl-jit")] pub use native::{ CompiledModelFunction, CompiledNativeModel, NativeAnalyticalModel, NativeExecutionArtifact, NativeOdeModel, NativeSdeModel, RuntimeBackend, }; pub use pharmsol_dsl::*; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] pub use runtime::{ - compile_execution_model_to_runtime, compile_module_source_to_runtime, load_runtime_artifact, - CompiledRuntimeModel, RuntimeAnalyticalModel, RuntimeArtifactFormat, RuntimeCompilationTarget, - RuntimeCovariateInfo, RuntimeError, RuntimeModelInfo, RuntimeOdeModel, RuntimeOutputInfo, - RuntimePredictions, RuntimeRouteInfo, RuntimeSdeModel, RuntimeStateInfo, + compile_execution_model_to_runtime, compile_module_source_to_runtime, CompiledRuntimeModel, + RuntimeAnalyticalModel, RuntimeCompilationTarget, RuntimeCovariateInfo, RuntimeError, + RuntimeModelInfo, RuntimeOdeModel, RuntimeOutputInfo, RuntimePredictions, RuntimeRouteInfo, + RuntimeSdeModel, RuntimeStateInfo, }; diff --git a/src/dsl/native.rs b/src/dsl/native.rs index f2fc1955..e3119092 100644 --- a/src/dsl/native.rs +++ b/src/dsl/native.rs @@ -12,8 +12,6 @@ use rayon::prelude::*; #[cfg(feature = "dsl-jit")] use cranelift_jit::JITModule; -#[cfg(feature = "dsl-aot-load")] -use libloading::Library; use pharmsol_dsl::execution::ModelFunctionKind; use pharmsol_dsl::{ AnalyticalKernel, AnalyticalStructureInputKind, AnalyticalStructureInputPlan, ModelKind, @@ -59,8 +57,6 @@ const DEFAULT_ODE_ATOL: f64 = 1e-4; pub enum RuntimeBackend { #[cfg(feature = "dsl-jit")] Jit, - #[cfg(feature = "dsl-aot-load")] - NativeAot, } pub(crate) trait FunctionSession { @@ -88,8 +84,6 @@ pub(crate) trait RuntimeArtifact: Send + Sync + std::fmt::Debug { enum NativeArtifactOwner { #[cfg(feature = "dsl-jit")] Jit(Box), - #[cfg(feature = "dsl-aot-load")] - Library(Library), } impl std::fmt::Debug for NativeArtifactOwner { @@ -97,9 +91,7 @@ impl std::fmt::Debug for NativeArtifactOwner { match self { #[cfg(feature = "dsl-jit")] Self::Jit(_) => _f.write_str("NativeArtifactOwner::Jit(..)"), - #[cfg(feature = "dsl-aot-load")] - Self::Library(_) => _f.write_str("NativeArtifactOwner::Library(..)"), - #[cfg(not(any(feature = "dsl-jit", feature = "dsl-aot-load")))] + #[cfg(not(feature = "dsl-jit"))] _ => unreachable!( "native artifact owner should only exist for supported native backends" ), @@ -170,34 +162,6 @@ impl NativeExecutionArtifact { _owner: Some(NativeArtifactOwner::Jit(Box::new(module))), } } - - #[cfg(feature = "dsl-aot-load")] - #[allow(clippy::too_many_arguments)] - pub(crate) fn from_library( - model_name: String, - derive: Option, - dynamics: Option, - outputs: CompiledModelFunction, - init: Option, - drift: Option, - diffusion: Option, - route_lag: Option, - route_bioavailability: Option, - library: Library, - ) -> Self { - Self { - model_name, - derive, - dynamics, - outputs, - init, - drift, - diffusion, - route_lag, - route_bioavailability, - _owner: Some(NativeArtifactOwner::Library(library)), - } - } } struct NativeFunctionSession<'a> { @@ -244,8 +208,6 @@ impl RuntimeArtifact for NativeExecutionArtifact { match &self._owner { #[cfg(feature = "dsl-jit")] Some(NativeArtifactOwner::Jit(_)) => RuntimeBackend::Jit, - #[cfg(feature = "dsl-aot-load")] - Some(NativeArtifactOwner::Library(_)) => RuntimeBackend::NativeAot, _ => unreachable!("native execution artifacts should always retain a supported owner"), } } @@ -2900,19 +2862,11 @@ mod tests { NativeOutputInfo, NativeRouteInfo, NativeSdeModel, NativeStateInfo, RuntimeArtifact, RuntimeBackend, SharedNativeModel, }; - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] use super::{ runtime_ode_predictions, BoundErrorModelCache, PredictionCache, DEFAULT_BOUND_ERROR_MODEL_CACHE_SIZE, DEFAULT_ODE_ATOL, DEFAULT_ODE_RTOL, }; use crate::PharmsolError; - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] use crate::{ data::builder::SubjectBuilderExt, dsl::{CompiledRuntimeModel, RuntimePredictions}, @@ -2925,10 +2879,6 @@ mod tests { AnalyticalKernel, AnalyticalStructureInputKind, CovariateInterpolation, ModelKind, RouteKind, }; - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] use std::sync::Arc; #[derive(Debug)] @@ -3233,10 +3183,6 @@ mod tests { .to_vec() } - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] fn cached_runtime_ode_model() -> NativeOdeModel { NativeOdeModel { shared: Arc::new(bolus_only_shared_model()), @@ -3250,10 +3196,6 @@ mod tests { } } - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] fn cached_runtime_subject() -> Subject { Subject::builder("runtime_cached_prediction") .bolus(0.0, 100.0, "oral") @@ -3402,10 +3344,6 @@ mod tests { )); } - #[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") - ))] #[test] fn compiled_runtime_ode_predictions_use_prefilled_cache() { let model = cached_runtime_ode_model(); diff --git a/src/dsl/runtime.rs b/src/dsl/runtime.rs index 216d283c..8c5d0411 100644 --- a/src/dsl/runtime.rs +++ b/src/dsl/runtime.rs @@ -1,16 +1,12 @@ //! Unified runtime entrypoints for DSL-backed models. //! //! Use this module when you already know you want an executable model and need -//! one backend-neutral surface for compile, load, and prediction workflows. -//! It normalizes the backend-specific JIT and native AoT entrypoints so -//! callers can choose a deployment target without rewriting the downstream -//! prediction code. +//! one backend-neutral surface for compile and prediction workflows. //! //! Use the backend modules directly only when you need a backend-specific //! artifact or compile control: //! //! - [`super::jit`] for direct in-process JIT compilation. -//! - [`compile_module_source_to_aot`][crate::dsl::compile_module_source_to_aot] for native artifact export and reload. //! //! Main entrypoints: //! @@ -18,18 +14,13 @@ //! path. //! - [`compile_execution_model_to_runtime`] when you already have an //! [`ExecutionModel`](pharmsol_dsl::ExecutionModel). -//! - [`load_runtime_artifact`] when the model has already been compiled and -//! stored elsewhere. //! - [`CompiledRuntimeModel::estimate_predictions`] for backend-neutral //! execution against a [`Subject`](crate::Subject). //! //! Backend choice guide: //! //! - [`RuntimeCompilationTarget::Jit`] keeps compilation and execution inside -//! the current process. Use it for native interactive workflows and tests. -//! - [`RuntimeCompilationTarget::NativeAot`] emits a native artifact and reloads -//! it into the same runtime model shape. Use it when you want reusable native -//! artifacts and can control the target platform. +//! the current process. //! //! Smallest compile-and-run example: //! @@ -75,15 +66,10 @@ //! ``` use std::fmt; -use std::path::Path; use ndarray::Array2; use thiserror::Error; -#[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] -use super::aot::{ - export_execution_model_to_aot, load_aot_model, AotError, NativeAotCompileOptions, -}; #[cfg(feature = "dsl-jit")] use super::jit::{compile_execution_model_to_jit, JitCompileError}; use super::native::{ @@ -119,17 +105,6 @@ pub enum RuntimeCompilationTarget { /// Compile and execute the model inside the current native process. #[cfg(feature = "dsl-jit")] Jit, - /// Export a native artifact and reload it as a runtime model. - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - NativeAot(NativeAotCompileOptions), -} - -/// Identifies the on-disk artifact format for [`load_runtime_artifact`]. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RuntimeArtifactFormat { - /// A native ahead-of-time artifact produced by the AoT compiler. - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - NativeAot, } /// Backend-neutral prediction output from a compiled runtime model. @@ -175,7 +150,7 @@ impl RuntimePredictions { /// Executable runtime model returned by the backend-neutral runtime surface. /// /// This type hides the concrete backend and keeps the prediction entrypoint the -/// same across JIT and native AoT-based flows. +/// same across runtime flows. #[derive(Clone, Debug)] pub enum CompiledRuntimeModel { Ode(RuntimeOdeModel), @@ -241,8 +216,8 @@ impl CompiledRuntimeModel { } } -/// Errors produced while parsing, lowering, compiling, loading, or executing a -/// runtime DSL model. +/// Errors produced while parsing, lowering, compiling, or executing a runtime +/// DSL model. #[derive(Error)] pub enum RuntimeError { #[error("failed to parse DSL source: {0}")] @@ -256,9 +231,6 @@ pub enum RuntimeError { #[cfg(feature = "dsl-jit")] #[error(transparent)] Jit(#[from] JitCompileError), - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - #[error(transparent)] - Aot(#[from] AotError), #[error(transparent)] Runtime(#[from] PharmsolError), } @@ -370,42 +342,17 @@ pub fn compile_execution_model_to_runtime( ); Ok(compiled.into()) } - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - RuntimeCompilationTarget::NativeAot(options) => { - let artifact = export_execution_model_to_aot(model, options, event_callback)?; - load_runtime_artifact(&artifact, RuntimeArtifactFormat::NativeAot) - } } } -/// Load a previously compiled native AoT artifact from disk. -pub fn load_runtime_artifact( - path: impl AsRef, - format: RuntimeArtifactFormat, -) -> Result { - #[cfg(not(all(feature = "dsl-aot", feature = "dsl-aot-load")))] - let _ = path.as_ref(); - match format { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - RuntimeArtifactFormat::NativeAot => Ok(load_aot_model(path)?.into()), - } -} - -#[cfg(all( - test, - feature = "dsl-jit", - feature = "dsl-aot", - feature = "dsl-aot-load" -))] +#[cfg(all(test, feature = "dsl-jit"))] mod tests { use super::*; use crate::dsl::compile_sde_model_to_jit; use crate::test_fixtures::STRUCTURED_BLOCK_CORPUS; use crate::PharmsolError; use crate::SubjectBuilderExt; - use approx::assert_relative_eq; use pharmsol_dsl::{DiagnosticPhase, RouteKind, DSL_BACKEND_GENERIC, DSL_PARSE_GENERIC}; - use tempfile::tempdir; const MULTI_DIGIT_OUTPUT_ORDER_RUNTIME_DSL: &str = r#" name = multi_digit_output_runtime @@ -544,30 +491,14 @@ out(cp) = central / v ~ continuous() .collect() } - fn compile_runtime_backend_matrix( - source: &str, - model_name: &str, - work_dir: &std::path::Path, - ) -> (CompiledRuntimeModel, CompiledRuntimeModel) { - let jit = compile_module_source_to_runtime( + fn compile_runtime_model(source: &str, model_name: &str) -> CompiledRuntimeModel { + compile_module_source_to_runtime( source, Some(model_name), RuntimeCompilationTarget::Jit, |_, _| {}, ) - .expect("compile jit runtime model"); - let aot = compile_module_source_to_runtime( - source, - Some(model_name), - RuntimeCompilationTarget::NativeAot( - NativeAotCompileOptions::new(work_dir.join(format!("{model_name}-aot-build"))) - .with_output(work_dir.join(format!("{model_name}.pkm"))), - ), - |_, _| {}, - ) - .expect("compile aot runtime model"); - - (jit, aot) + .expect("compile jit runtime model") } fn compiled_route_input_index(model: &CompiledRuntimeModel, name: &str) -> Option { @@ -683,32 +614,13 @@ out(cp) = central / v ~ continuous() } #[test] - fn runtime_backend_matrix_matches_ode_predictions() { - let work_dir = tempdir().expect("tempdir"); - - let jit = compile_module_source_to_runtime( - corpus_source(), - Some("one_cmt_oral_iv"), - RuntimeCompilationTarget::Jit, - |_, _| {}, - ) - .expect("compile jit runtime model"); - let aot = compile_module_source_to_runtime( - corpus_source(), - Some("one_cmt_oral_iv"), - RuntimeCompilationTarget::NativeAot( - NativeAotCompileOptions::new(work_dir.path().join("aot-build")) - .with_output(work_dir.path().join("one_cmt_oral_iv.pkm")), - ), - |_, _| {}, - ) - .expect("compile aot runtime model"); + fn runtime_jit_matches_ode_predictions() { + let jit = compile_runtime_model(corpus_source(), "one_cmt_oral_iv"); assert_eq!(jit.backend(), RuntimeBackend::Jit); - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); assert_eq!(jit.info().name, "one_cmt_oral_iv"); assert_eq!( - aot.info().parameters, + jit.info().parameters, vec!["ka", "cl", "v", "tlag", "f_oral"] ); let support = Parameters::with_model( @@ -732,21 +644,13 @@ out(cp) = central / v ~ continuous() &jit.estimate_predictions(&subject, &support) .expect("jit predictions"), ); - let aot_values = subject_values( - &aot.estimate_predictions(&subject, &support) - .expect("aot predictions"), - ); - - for (jit_value, aot_value) in jit_values.iter().zip(aot_values.iter()) { - assert_relative_eq!(jit_value, aot_value, max_relative = 1e-4); - } + assert_eq!(jit_values.len(), 6); + assert!(jit_values.iter().all(|value| value.is_finite())); } #[test] - fn runtime_backend_matrix_kindless_routes_accept_both_input_kinds() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = - compile_runtime_backend_matrix(corpus_source(), "one_cmt_oral_iv", work_dir.path()); + fn runtime_jit_kindless_routes_accept_both_input_kinds() { + let jit = compile_runtime_model(corpus_source(), "one_cmt_oral_iv"); let support = Parameters::with_model( &jit, [ @@ -762,21 +666,19 @@ out(cp) = central / v ~ continuous() // Canonical `model {}` routes carry no kind and keep their declaration // ordinals. A future collapse of `None` to `Some(Bolus)` anywhere in // the lowering pipeline must fail here, close to its source. - for model in [&jit, &aot] { - let routes = &model.info().routes; - let oral = routes - .iter() - .find(|route| route.name == "oral") - .expect("oral route"); - let iv = routes - .iter() - .find(|route| route.name == "iv") - .expect("iv route"); - assert_eq!(oral.kind, None, "oral route kind collapsed"); - assert_eq!(iv.kind, None, "iv route kind collapsed"); - assert_eq!(oral.index, 0); - assert_eq!(iv.index, 1); - } + let routes = &jit.info().routes; + let oral = routes + .iter() + .find(|route| route.name == "oral") + .expect("oral route"); + let iv = routes + .iter() + .find(|route| route.name == "iv") + .expect("iv route"); + assert_eq!(oral.kind, None, "oral route kind collapsed"); + assert_eq!(iv.kind, None, "iv route kind collapsed"); + assert_eq!(oral.index, 0); + assert_eq!(iv.index, 1); // A kindless route is usable as either input kind: bolus and infusion // events both resolve through the same declaration, in both the @@ -802,20 +704,14 @@ out(cp) = central / v ~ continuous() .missing_observation(1.0, "cp") .build(); - for model in [&jit, &aot] { - model - .estimate_predictions(&natural_bolus, &support) - .expect("bolus oral resolves on kindless route"); - model - .estimate_predictions(&natural_infusion, &support) - .expect("infusion iv resolves on kindless route"); - model - .estimate_predictions(&cross_bolus, &support) - .expect("bolus iv resolves on kindless route"); - model - .estimate_predictions(&cross_infusion, &support) - .expect("infusion oral resolves on kindless route"); - } + jit.estimate_predictions(&natural_bolus, &support) + .expect("bolus oral resolves on kindless route"); + jit.estimate_predictions(&natural_infusion, &support) + .expect("infusion iv resolves on kindless route"); + jit.estimate_predictions(&cross_bolus, &support) + .expect("bolus iv resolves on kindless route"); + jit.estimate_predictions(&cross_infusion, &support) + .expect("infusion oral resolves on kindless route"); } #[test] @@ -840,169 +736,122 @@ out(cp) = central / v ~ continuous() } #[test] - fn runtime_backend_matrix_reports_route_kind_mismatch() { - let work_dir = tempdir().expect("tempdir"); + fn runtime_jit_reports_route_kind_mismatch() { let subject = mismatched_route_kind_subject(); - let (jit, aot) = compile_runtime_backend_matrix( + let jit = compile_runtime_model( NUMERIC_ROUTE_LABELS_RUNTIME_DSL, "prefixed_numeric_route_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); let expected_input = compiled_route_input_index(&jit, "input_10").expect("input_10 route index"); - for model in [&jit, &aot] { - assert_unsupported_input_route_kind( - model, - &subject, - &support, - expected_input, - RouteKind::Infusion, - ); - } + assert_unsupported_input_route_kind( + &jit, + &subject, + &support, + expected_input, + RouteKind::Infusion, + ); } #[test] - fn runtime_backend_matrix_preserves_multi_digit_output_label_order() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_preserves_multi_digit_output_label_order() { + let jit = compile_runtime_model( MULTI_DIGIT_OUTPUT_ORDER_RUNTIME_DSL, "multi_digit_output_runtime", - work_dir.path(), ); assert_eq!(compiled_output_slot_index(&jit, "outeq_2"), Some(0)); assert_eq!(compiled_output_slot_index(&jit, "outeq_10"), Some(1)); assert_eq!(compiled_output_slot_index(&jit, "outeq_11"), Some(2)); - assert_eq!(compiled_output_slot_index(&aot, "outeq_2"), Some(0)); - assert_eq!(compiled_output_slot_index(&aot, "outeq_10"), Some(1)); - assert_eq!(compiled_output_slot_index(&aot, "outeq_11"), Some(2)); } #[test] - fn runtime_backend_matrix_supports_prefixed_multi_digit_numeric_route_labels() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_supports_prefixed_multi_digit_numeric_route_labels() { + let jit = compile_runtime_model( NUMERIC_ROUTE_LABELS_RUNTIME_DSL, "prefixed_numeric_route_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); assert_eq!(compiled_route_input_index(&jit, "input_10"), Some(0)); assert_eq!(compiled_route_input_index(&jit, "input_11"), Some(1)); - assert_eq!(compiled_route_input_index(&aot, "input_10"), Some(0)); - assert_eq!(compiled_route_input_index(&aot, "input_11"), Some(1)); let subject = numeric_route_subject(); - let jit_values = subject_values( + let values = subject_values( &jit.estimate_predictions(&subject, &support) .expect("jit predictions"), ); - let aot_values = subject_values( - &aot.estimate_predictions(&subject, &support) - .expect("aot predictions"), - ); - for (jit_value, aot_value) in jit_values.iter().zip(aot_values.iter()) { - assert_relative_eq!(jit_value, aot_value, max_relative = 1e-4); - } + assert!(values.iter().all(|value| value.is_finite())); } #[test] - fn runtime_backend_matrix_resolves_raw_numeric_route_labels_against_prefixed_metadata() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_resolves_raw_numeric_route_labels_against_prefixed_metadata() { + let jit = compile_runtime_model( NUMERIC_ROUTE_LABELS_RUNTIME_DSL, "prefixed_numeric_route_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); let subject = numeric_route_alias_subject(); - let jit_values = subject_values( + let values = subject_values( &jit.estimate_predictions(&subject, &support) .expect("jit predictions"), ); - let aot_values = subject_values( - &aot.estimate_predictions(&subject, &support) - .expect("aot predictions"), - ); - for (jit_value, aot_value) in jit_values.iter().zip(aot_values.iter()) { - assert_relative_eq!(jit_value, aot_value, max_relative = 1e-4); - } + assert!(values.iter().all(|value| value.is_finite())); } #[test] - fn runtime_backend_matrix_supports_prefixed_numeric_route_and_output_labels() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_supports_prefixed_numeric_route_and_output_labels() { + let jit = compile_runtime_model( SHARED_NUMERIC_ROUTE_OUTPUT_LABEL_RUNTIME_DSL, "prefixed_numeric_route_output_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); assert_eq!(compiled_route_input_index(&jit, "input_1"), Some(0)); assert_eq!(compiled_output_slot_index(&jit, "outeq_1"), Some(0)); - assert_eq!(compiled_route_input_index(&aot, "input_1"), Some(0)); - assert_eq!(compiled_output_slot_index(&aot, "outeq_1"), Some(0)); let subject = shared_numeric_route_output_subject(); - let jit_values = subject_values( + let values = subject_values( &jit.estimate_predictions(&subject, &support) .expect("jit predictions"), ); - let aot_values = subject_values( - &aot.estimate_predictions(&subject, &support) - .expect("aot predictions"), - ); - for (jit_value, aot_value) in jit_values.iter().zip(aot_values.iter()) { - assert_relative_eq!(jit_value, aot_value, max_relative = 1e-4); - } + assert!(values.iter().all(|value| value.is_finite())); } #[test] - fn runtime_backend_matrix_resolves_shared_raw_numeric_route_and_output_aliases() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_resolves_shared_raw_numeric_route_and_output_aliases() { + let jit = compile_runtime_model( SHARED_NUMERIC_ROUTE_OUTPUT_LABEL_RUNTIME_DSL, "prefixed_numeric_route_output_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); let subject = shared_numeric_route_output_alias_subject(); - let jit_values = subject_values( + let values = subject_values( &jit.estimate_predictions(&subject, &support) .expect("jit predictions"), ); - let aot_values = subject_values( - &aot.estimate_predictions(&subject, &support) - .expect("aot predictions"), - ); - for (jit_value, aot_value) in jit_values.iter().zip(aot_values.iter()) { - assert_relative_eq!(jit_value, aot_value, max_relative = 1e-4); - } + assert!(values.iter().all(|value| value.is_finite())); } #[test] - fn runtime_backend_matrix_rejects_undeclared_numeric_output_labels() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_rejects_undeclared_numeric_output_labels() { + let jit = compile_runtime_model( UNDECLARED_NUMERIC_OUTPUT_LABEL_RUNTIME_DSL, "undeclared_numeric_output_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); @@ -1012,16 +861,13 @@ out(cp) = central / v ~ continuous() .build(); assert_unknown_output_label(&jit, &subject, &support, "10"); - assert_unknown_output_label(&aot, &subject, &support, "10"); } #[test] - fn runtime_backend_matrix_rejects_undeclared_numeric_input_labels() { - let work_dir = tempdir().expect("tempdir"); - let (jit, aot) = compile_runtime_backend_matrix( + fn runtime_jit_rejects_undeclared_numeric_input_labels() { + let jit = compile_runtime_model( UNDECLARED_NUMERIC_INPUT_LABEL_RUNTIME_DSL, "undeclared_numeric_input_runtime", - work_dir.path(), ); let support = Parameters::with_model(&jit, [("ke", 0.2), ("v", 10.0)]) .expect("valid named parameters"); @@ -1031,7 +877,6 @@ out(cp) = central / v ~ continuous() .build(); assert_unknown_input_label(&jit, &subject, &support, "10"); - assert_unknown_input_label(&aot, &subject, &support, "10"); } #[test] diff --git a/src/dsl/rust_backend.rs b/src/dsl/rust_backend.rs deleted file mode 100644 index d4207037..00000000 --- a/src/dsl/rust_backend.rs +++ /dev/null @@ -1,490 +0,0 @@ -use std::fmt::Write; - -use super::compiled_backend_abi::{ - compiled_function_symbol, encode_compiled_model_info, API_VERSION_SYMBOL, - MODEL_INFO_JSON_LEN_SYMBOL, MODEL_INFO_JSON_PTR_SYMBOL, -}; -use pharmsol_dsl::execution::{ - ExecutionBlock, ExecutionCall, ExecutionExpr, ExecutionExprKind, ExecutionLoad, ExecutionModel, - ExecutionProgram, ExecutionStateRef, ExecutionStmt, ExecutionStmtKind, ExecutionTargetKind, - FunctionBody, -}; -use pharmsol_dsl::{AnalyzedBinaryOp, AnalyzedUnaryOp, MathFunction, ValueType}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RustBackendFlavor { - #[cfg(feature = "dsl-aot")] - NativeAot { api_version: u32 }, -} - -impl RustBackendFlavor { - fn api_version(self) -> u32 { - match self { - #[cfg(feature = "dsl-aot")] - Self::NativeAot { api_version } => api_version, - } - } -} - -pub fn emit_rust_backend_source( - model: &ExecutionModel, - flavor: RustBackendFlavor, -) -> Result { - let model_info_json = encode_compiled_model_info(model, flavor.api_version()) - .map_err(|error| error.to_string())?; - - let mut source = String::new(); - writeln!(source, "#![allow(dead_code)]").unwrap(); - writeln!(source, "#![allow(unused_mut)]").unwrap(); - writeln!(source, "#![allow(unused_variables)]").unwrap(); - writeln!(source).unwrap(); - writeln!( - source, - "const MODEL_INFO_JSON: &str = {:?};", - model_info_json - ) - .unwrap(); - writeln!( - source, - "const PHARMSOL_DSL_API_VERSION: u32 = {};", - flavor.api_version() - ) - .unwrap(); - writeln!(source).unwrap(); - writeln!(source, "#[inline]").unwrap(); - writeln!( - source, - "unsafe fn load_f64(ptr: *const f64, index: usize) -> f64 {{ *ptr.add(index) }}" - ) - .unwrap(); - writeln!(source, "#[inline]").unwrap(); - writeln!(source, "unsafe fn store_f64(ptr: *mut f64, index: usize, value: f64) {{ *ptr.add(index) = value; }}").unwrap(); - writeln!(source).unwrap(); - writeln!(source, "#[no_mangle]").unwrap(); - writeln!( - source, - "pub extern \"C\" fn {API_VERSION_SYMBOL}() -> u32 {{ PHARMSOL_DSL_API_VERSION }}" - ) - .unwrap(); - writeln!(source, "#[no_mangle]").unwrap(); - writeln!(source, "pub extern \"C\" fn {MODEL_INFO_JSON_PTR_SYMBOL}() -> *const u8 {{ MODEL_INFO_JSON.as_ptr() }}").unwrap(); - writeln!(source, "#[no_mangle]").unwrap(); - writeln!( - source, - "pub extern \"C\" fn {MODEL_INFO_JSON_LEN_SYMBOL}() -> usize {{ MODEL_INFO_JSON.len() }}" - ) - .unwrap(); - writeln!(source).unwrap(); - - for function in &model.functions { - if let Some(symbol) = compiled_function_symbol(function.kind) { - if let FunctionBody::Statements(program) = &function.body { - emit_statement_function(&mut source, program, symbol)?; - writeln!(source).unwrap(); - } - } - } - - Ok(source) -} -fn emit_statement_function( - source: &mut String, - program: &ExecutionProgram, - symbol: &'static str, -) -> Result<(), String> { - writeln!(source, "#[no_mangle]").unwrap(); - writeln!(source, "pub unsafe extern \"C\" fn {symbol}(").unwrap(); - writeln!(source, " t: f64,").unwrap(); - writeln!(source, " states: *const f64,").unwrap(); - writeln!(source, " params: *const f64,").unwrap(); - writeln!(source, " covariates: *const f64,").unwrap(); - writeln!(source, " routes: *const f64,").unwrap(); - writeln!(source, " derived: *const f64,").unwrap(); - writeln!(source, " out: *mut f64,").unwrap(); - writeln!(source, ") {{").unwrap(); - - for local in &program.locals { - writeln!( - source, - " let mut local_{}: {} = {};", - local.index, - rust_type(local.ty), - rust_zero(local.ty) - ) - .unwrap(); - } - - emit_block(source, &program.body, 1)?; - writeln!(source, "}}").unwrap(); - Ok(()) -} - -fn emit_block(source: &mut String, block: &ExecutionBlock, indent: usize) -> Result<(), String> { - for statement in &block.statements { - emit_stmt(source, statement, indent)?; - } - Ok(()) -} - -fn emit_stmt(source: &mut String, statement: &ExecutionStmt, indent: usize) -> Result<(), String> { - match &statement.kind { - ExecutionStmtKind::Let(let_stmt) => { - let value = emit_expr(&let_stmt.value)?; - push_line( - source, - indent, - &format!("local_{} = {};", let_stmt.local, value.rendered), - ); - } - ExecutionStmtKind::Assign(assign) => { - let value = emit_expr(&assign.value)?; - let value = cast_expr(value.rendered, value.ty, ValueType::Real); - let target = emit_target(&assign.target.kind)?; - push_line( - source, - indent, - &format!("unsafe {{ store_f64(out, {target}, {value}); }}"), - ); - } - ExecutionStmtKind::If(if_stmt) => { - let condition = emit_expr(&if_stmt.condition)?; - let condition = cast_expr(condition.rendered, condition.ty, ValueType::Bool); - push_line(source, indent, &format!("if {condition} {{")); - for nested in &if_stmt.then_branch { - emit_stmt(source, nested, indent + 1)?; - } - if let Some(else_branch) = &if_stmt.else_branch { - push_line(source, indent, "} else {"); - for nested in else_branch { - emit_stmt(source, nested, indent + 1)?; - } - } - push_line(source, indent, "}"); - } - ExecutionStmtKind::For(for_stmt) => { - let start = emit_expr(&for_stmt.range.start)?; - let end = emit_expr(&for_stmt.range.end)?; - let start = cast_expr(start.rendered, start.ty, ValueType::Int); - let end = cast_expr(end.rendered, end.ty, ValueType::Int); - push_line( - source, - indent, - &format!( - "for __loop_local_{} in ({start})..({end}) {{", - for_stmt.local - ), - ); - push_line( - source, - indent + 1, - &format!( - "local_{} = __loop_local_{};", - for_stmt.local, for_stmt.local - ), - ); - for nested in &for_stmt.body { - emit_stmt(source, nested, indent + 1)?; - } - push_line(source, indent, "}"); - } - } - Ok(()) -} - -fn emit_target(target: &ExecutionTargetKind) -> Result { - Ok(match target { - ExecutionTargetKind::Derived(index) - | ExecutionTargetKind::Output(index) - | ExecutionTargetKind::RouteLag(index) - | ExecutionTargetKind::RouteBioavailability(index) => index.to_string(), - ExecutionTargetKind::StateInit(state) - | ExecutionTargetKind::StateDerivative(state) - | ExecutionTargetKind::StateNoise(state) => emit_state_ref_index(state)?, - }) -} - -fn emit_state_ref_index(state: &ExecutionStateRef) -> Result { - Ok(match &state.index { - Some(index) => { - let index = emit_expr(index)?; - let index = cast_expr(index.rendered, index.ty, ValueType::Int); - format!("{} + ({index} as usize)", state.base_offset) - } - None => state.base_offset.to_string(), - }) -} - -#[derive(Debug, Clone)] -struct RenderedExpr { - rendered: String, - ty: ValueType, -} - -fn emit_expr(expr: &ExecutionExpr) -> Result { - let rendered = match &expr.kind { - ExecutionExprKind::Literal(value) => match value { - super::ConstValue::Int(value) => format!("{value}i64"), - super::ConstValue::Real(value) => format!("{value:?}"), - super::ConstValue::Bool(value) => value.to_string(), - }, - ExecutionExprKind::Load(load) => emit_load(load, expr.ty)?, - ExecutionExprKind::Unary { op, expr: inner } => { - let inner = emit_expr(inner)?; - match op { - AnalyzedUnaryOp::Plus => cast_expr(inner.rendered, inner.ty, expr.ty), - AnalyzedUnaryOp::Minus => match expr.ty { - ValueType::Real | ValueType::Int => { - format!("-({})", cast_expr(inner.rendered, inner.ty, expr.ty)) - } - ValueType::Bool => { - return Err("cannot emit unary minus for boolean expressions".to_string()) - } - }, - AnalyzedUnaryOp::Not => { - format!( - "!({})", - cast_expr(inner.rendered, inner.ty, ValueType::Bool) - ) - } - } - } - ExecutionExprKind::Binary { op, lhs, rhs } => emit_binary_expr(*op, lhs, rhs, expr.ty)?, - ExecutionExprKind::Call { callee, args } => emit_call_expr(callee, args, expr.ty)?, - }; - - Ok(RenderedExpr { - rendered, - ty: expr.ty, - }) -} - -fn emit_load(load: &ExecutionLoad, ty: ValueType) -> Result { - let raw = match load { - ExecutionLoad::Time => "t".to_string(), - ExecutionLoad::Parameter(index) => format!("load_f64(params, {index})"), - ExecutionLoad::Covariate(index) => format!("load_f64(covariates, {index})"), - ExecutionLoad::Derived(index) => format!("load_f64(derived, {index})"), - ExecutionLoad::Local(index) => return Ok(format!("local_{index}")), - ExecutionLoad::RouteInput { index, .. } => format!("load_f64(routes, {index})"), - ExecutionLoad::State(state) => { - let index = emit_state_ref_index(state)?; - format!("load_f64(states, {index})") - } - }; - Ok(cast_expr(raw, ValueType::Real, ty)) -} - -fn emit_binary_expr( - op: AnalyzedBinaryOp, - lhs: &ExecutionExpr, - rhs: &ExecutionExpr, - result_ty: ValueType, -) -> Result { - let lhs = emit_expr(lhs)?; - let rhs = emit_expr(rhs)?; - Ok(match op { - AnalyzedBinaryOp::Or => format!( - "({}) || ({})", - cast_expr(lhs.rendered, lhs.ty, ValueType::Bool), - cast_expr(rhs.rendered, rhs.ty, ValueType::Bool) - ), - AnalyzedBinaryOp::And => format!( - "({}) && ({})", - cast_expr(lhs.rendered, lhs.ty, ValueType::Bool), - cast_expr(rhs.rendered, rhs.ty, ValueType::Bool) - ), - AnalyzedBinaryOp::Eq | AnalyzedBinaryOp::NotEq => { - let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { - ValueType::Real - } else if lhs.ty == ValueType::Bool && rhs.ty == ValueType::Bool { - ValueType::Bool - } else { - ValueType::Int - }; - let operator = if op == AnalyzedBinaryOp::Eq { - "==" - } else { - "!=" - }; - format!( - "({}) {operator} ({})", - cast_expr(lhs.rendered, lhs.ty, operand_ty), - cast_expr(rhs.rendered, rhs.ty, operand_ty) - ) - } - AnalyzedBinaryOp::Lt - | AnalyzedBinaryOp::LtEq - | AnalyzedBinaryOp::Gt - | AnalyzedBinaryOp::GtEq => { - let operand_ty = if lhs.ty == ValueType::Real || rhs.ty == ValueType::Real { - ValueType::Real - } else { - ValueType::Int - }; - let operator = match op { - AnalyzedBinaryOp::Lt => "<", - AnalyzedBinaryOp::LtEq => "<=", - AnalyzedBinaryOp::Gt => ">", - AnalyzedBinaryOp::GtEq => ">=", - _ => unreachable!(), - }; - format!( - "({}) {operator} ({})", - cast_expr(lhs.rendered, lhs.ty, operand_ty), - cast_expr(rhs.rendered, rhs.ty, operand_ty) - ) - } - AnalyzedBinaryOp::Add | AnalyzedBinaryOp::Sub | AnalyzedBinaryOp::Mul => { - let operator = match op { - AnalyzedBinaryOp::Add => "+", - AnalyzedBinaryOp::Sub => "-", - AnalyzedBinaryOp::Mul => "*", - _ => unreachable!(), - }; - format!( - "({}) {operator} ({})", - cast_expr(lhs.rendered, lhs.ty, result_ty), - cast_expr(rhs.rendered, rhs.ty, result_ty) - ) - } - AnalyzedBinaryOp::Div => format!( - "({}) / ({})", - cast_expr(lhs.rendered, lhs.ty, ValueType::Real), - cast_expr(rhs.rendered, rhs.ty, ValueType::Real) - ), - AnalyzedBinaryOp::Pow => { - let lhs = cast_expr(lhs.rendered, lhs.ty, ValueType::Real); - let rhs = cast_expr(rhs.rendered, rhs.ty, ValueType::Real); - cast_expr(format!("({lhs}).powf({rhs})"), ValueType::Real, result_ty) - } - }) -} - -fn emit_call_expr( - callee: &ExecutionCall, - args: &[ExecutionExpr], - result_ty: ValueType, -) -> Result { - match callee { - ExecutionCall::Math(intrinsic) => emit_math_call(*intrinsic, args, result_ty), - } -} - -fn emit_math_call( - intrinsic: MathFunction, - args: &[ExecutionExpr], - result_ty: ValueType, -) -> Result { - let args = args.iter().map(emit_expr).collect::, _>>()?; - Ok(match intrinsic { - MathFunction::Max | MathFunction::Min => { - if args.len() != 2 { - return Err(format!("{intrinsic:?} expects 2 arguments")); - } - match result_ty { - ValueType::Real => { - let lhs = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Real); - let rhs = cast_expr(args[1].rendered.clone(), args[1].ty, ValueType::Real); - let method = if intrinsic == MathFunction::Max { - "max" - } else { - "min" - }; - format!("({lhs}).{method}({rhs})") - } - ValueType::Int => { - let lhs = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Int); - let rhs = cast_expr(args[1].rendered.clone(), args[1].ty, ValueType::Int); - let function = if intrinsic == MathFunction::Max { - "std::cmp::max" - } else { - "std::cmp::min" - }; - format!("{function}({lhs}, {rhs})") - } - ValueType::Bool => { - return Err("min/max do not accept boolean arguments".to_string()) - } - } - } - MathFunction::Abs if result_ty == ValueType::Int => { - let value = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Int); - format!("({value}).abs()") - } - _ => { - let function = match intrinsic { - MathFunction::Abs => "abs", - MathFunction::Ceil => "ceil", - MathFunction::Exp => "exp", - MathFunction::Floor => "floor", - MathFunction::Ln | MathFunction::Log => "ln", - MathFunction::Log10 => "log10", - MathFunction::Log2 => "log2", - MathFunction::Pow => { - if args.len() != 2 { - return Err("pow expects 2 arguments".to_string()); - } - let lhs = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Real); - let rhs = cast_expr(args[1].rendered.clone(), args[1].ty, ValueType::Real); - return Ok(cast_expr( - format!("({lhs}).powf({rhs})"), - ValueType::Real, - result_ty, - )); - } - MathFunction::Round => "round", - MathFunction::Sin => "sin", - MathFunction::Cos => "cos", - MathFunction::Tan => "tan", - MathFunction::Sqrt => "sqrt", - MathFunction::Max | MathFunction::Min => unreachable!(), - }; - let value = cast_expr(args[0].rendered.clone(), args[0].ty, ValueType::Real); - cast_expr( - format!("({value}).{function}()"), - ValueType::Real, - result_ty, - ) - } - }) -} - -fn cast_expr(expr: String, from: ValueType, to: ValueType) -> String { - if from == to { - return expr; - } - - match (from, to) { - (ValueType::Int, ValueType::Real) => format!("({expr}) as f64"), - (ValueType::Bool, ValueType::Real) => format!("if {expr} {{ 1.0 }} else {{ 0.0 }}"), - (ValueType::Real, ValueType::Int) => format!("({expr}) as i64"), - (ValueType::Bool, ValueType::Int) => format!("if {expr} {{ 1i64 }} else {{ 0i64 }}"), - (ValueType::Real, ValueType::Bool) => format!("({expr}) != 0.0"), - (ValueType::Int, ValueType::Bool) => format!("({expr}) != 0"), - _ => expr, - } -} - -fn rust_type(ty: ValueType) -> &'static str { - match ty { - ValueType::Int => "i64", - ValueType::Real => "f64", - ValueType::Bool => "bool", - } -} - -fn rust_zero(ty: ValueType) -> &'static str { - match ty { - ValueType::Int => "0i64", - ValueType::Real => "0.0", - ValueType::Bool => "false", - } -} - -fn push_line(source: &mut String, indent: usize, line: &str) { - for _ in 0..indent { - source.push_str(" "); - } - source.push_str(line); - source.push('\n'); -} diff --git a/src/lib.rs b/src/lib.rs index edabc6a0..603e474c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,8 +80,6 @@ //! //! - `dsl-core`: exposes the `pharmsol::dsl` facade and DSL compiler types //! - `dsl-jit`: adds in-process JIT compilation -//! - `dsl-aot`: adds native ahead-of-time artifact compilation -//! - `dsl-aot-load`: adds native artifact loading //! //! ## Labels And Indices //! @@ -108,8 +106,6 @@ // Lets `ode!`, `analytical!`, and `sde!` expand to `::pharmsol::…` inside this crate too. extern crate self as pharmsol; -#[cfg(feature = "dsl-aot")] -mod build_support; pub mod data; #[cfg(feature = "dsl-core")] pub mod dsl; diff --git a/src/parameters.rs b/src/parameters.rs index 3f4c6e96..bdd59175 100644 --- a/src/parameters.rs +++ b/src/parameters.rs @@ -5,10 +5,7 @@ use thiserror::Error; use crate::parameter_order::{ParameterOrderError, ParameterOrderPlan}; -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] use crate::dsl::{CompiledRuntimeModel, RuntimeAnalyticalModel, RuntimeOdeModel, RuntimeSdeModel}; use crate::{Analytical, ODE, SDE}; @@ -221,10 +218,7 @@ impl NamedParameterModel for SDE { } } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] impl NamedParameterModel for CompiledRuntimeModel { fn parameter_order_plan(&self, source_names: S) -> Result where @@ -236,10 +230,7 @@ impl NamedParameterModel for CompiledRuntimeModel { } } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] impl NamedParameterModel for RuntimeOdeModel { fn parameter_order_plan(&self, source_names: S) -> Result where @@ -251,10 +242,7 @@ impl NamedParameterModel for RuntimeOdeModel { } } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] impl NamedParameterModel for RuntimeAnalyticalModel { fn parameter_order_plan(&self, source_names: S) -> Result where @@ -266,10 +254,7 @@ impl NamedParameterModel for RuntimeAnalyticalModel { } } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] impl NamedParameterModel for RuntimeSdeModel { fn parameter_order_plan(&self, source_names: S) -> Result where diff --git a/src/simulator/equation/ode/mod.rs b/src/simulator/equation/ode/mod.rs index c90128ab..18d28e13 100644 --- a/src/simulator/equation/ode/mod.rs +++ b/src/simulator/equation/ode/mod.rs @@ -5,7 +5,7 @@ mod closure; /// /// This helper is shared by the legacy JIT path and the native /// runtime wrappers. -#[cfg(any(feature = "dsl-jit", feature = "dsl-aot-load"))] +#[cfg(feature = "dsl-jit")] pub(crate) mod closure_helpers { pub(crate) use super::closure::PMProblem; } diff --git a/tests/bimodal_ke_entrypoint_matrix.rs b/tests/bimodal_ke_entrypoint_matrix.rs index e08e2d19..9b1b0be6 100644 --- a/tests/bimodal_ke_entrypoint_matrix.rs +++ b/tests/bimodal_ke_entrypoint_matrix.rs @@ -1,7 +1,7 @@ #[path = "support/bimodal_ke.rs"] mod bimodal_ke; -#[cfg(all(feature = "dsl-jit", feature = "dsl-aot", feature = "dsl-aot-load"))] +#[cfg(feature = "dsl-jit")] mod tests { use super::bimodal_ke; use pharmsol::dsl::RuntimeBackend; @@ -17,24 +17,6 @@ mod tests { 1e-10, )?; - let runtime_aot_workspace = bimodal_ke::ArtifactWorkspace::new()?; - let runtime_aot = bimodal_ke::compile_runtime_native_aot_model(&runtime_aot_workspace)?; - assert_eq!(runtime_aot.backend(), RuntimeBackend::NativeAot); - bimodal_ke::report_runtime_model( - "dsl::compile_module_source_to_runtime(NativeAot)", - &runtime_aot, - 1e-10, - )?; - - let direct_aot_workspace = bimodal_ke::ArtifactWorkspace::new()?; - let direct_aot = bimodal_ke::compile_direct_aot_model(&direct_aot_workspace)?; - assert_eq!(direct_aot.backend(), RuntimeBackend::NativeAot); - bimodal_ke::report_runtime_model( - "dsl::compile_module_source_to_aot + load_runtime_artifact", - &direct_aot, - 1e-10, - )?; - Ok(()) } } diff --git a/tests/full_feature_dsl_backend_parity.rs b/tests/full_feature_dsl_backend_parity.rs index 4c89659d..ac42e1f6 100644 --- a/tests/full_feature_dsl_backend_parity.rs +++ b/tests/full_feature_dsl_backend_parity.rs @@ -10,19 +10,6 @@ mod tests { names.iter().map(|name| (*name).to_owned()).collect() } - fn assert_info_matches( - left_label: &str, - left: &CompiledRuntimeModel, - right_label: &str, - right: &CompiledRuntimeModel, - ) { - assert_eq!( - left.info(), - right.info(), - "{left_label} model info diverged from {right_label}" - ); - } - fn assert_ode_full_public_shape(model: &CompiledRuntimeModel) { let info = model.info(); @@ -137,35 +124,11 @@ mod tests { case: CorpusCase, assert_public_shape: fn(&CompiledRuntimeModel), ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(case)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); assert_public_shape(&jit); corpus::assert_runtime_model_matches_reference(case, "runtime-jit", &jit)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(case, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - { - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - assert_public_shape(&aot); - corpus::assert_runtime_model_matches_reference(case, "runtime-native-aot", &aot)?; - } - - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - { - assert_info_matches("runtime-jit", &jit, "runtime-native-aot", &aot); - corpus::assert_runtime_models_match_each_other( - case, - "runtime-jit", - &jit, - "runtime-native-aot", - &aot, - )?; - } - Ok(()) } #[test] diff --git a/tests/runtime_backend_matrix.rs b/tests/runtime_backend_matrix.rs index 69493be7..af3f5c02 100644 --- a/tests/runtime_backend_matrix.rs +++ b/tests/runtime_backend_matrix.rs @@ -9,33 +9,16 @@ mod tests { #[test] fn ode_runtime_backend_matrix_matches_reference_predictions( ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(CorpusCase::Ode)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); corpus::assert_runtime_model_matches_reference(CorpusCase::Ode, "runtime-jit", &jit)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(CorpusCase::Ode, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - corpus::assert_runtime_model_matches_reference( - CorpusCase::Ode, - "runtime-native-aot", - &aot, - )?; - Ok(()) } #[test] fn analytical_runtime_backend_matrix_matches_reference_predictions( ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(CorpusCase::Analytical)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); corpus::assert_runtime_model_matches_reference( @@ -44,26 +27,12 @@ mod tests { &jit, )?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(CorpusCase::Analytical, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - corpus::assert_runtime_model_matches_reference( - CorpusCase::Analytical, - "runtime-native-aot", - &aot, - )?; - Ok(()) } #[test] fn analytical_full_runtime_backend_matrix_matches_reference_predictions( ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(CorpusCase::AnalyticalFull)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); corpus::assert_runtime_model_matches_reference( @@ -72,65 +41,26 @@ mod tests { &jit, )?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(CorpusCase::AnalyticalFull, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - corpus::assert_runtime_model_matches_reference( - CorpusCase::AnalyticalFull, - "runtime-native-aot", - &aot, - )?; - Ok(()) } #[test] fn ode_full_runtime_backend_matrix_matches_reference_predictions( ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(CorpusCase::OdeFull)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); corpus::assert_runtime_model_matches_reference(CorpusCase::OdeFull, "runtime-jit", &jit)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(CorpusCase::OdeFull, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - corpus::assert_runtime_model_matches_reference( - CorpusCase::OdeFull, - "runtime-native-aot", - &aot, - )?; - Ok(()) } #[test] fn sde_runtime_backend_matrix_matches_reference_predictions( ) -> Result<(), Box> { - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let workspace = super::runtime_corpus::ArtifactWorkspace::new()?; - let jit = corpus::compile_runtime_jit_model(CorpusCase::Sde)?; assert_eq!(jit.backend(), RuntimeBackend::Jit); corpus::assert_runtime_model_matches_reference(CorpusCase::Sde, "runtime-jit", &jit)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - let aot = corpus::compile_runtime_native_aot_model(CorpusCase::Sde, &workspace)?; - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - assert_eq!(aot.backend(), RuntimeBackend::NativeAot); - #[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] - corpus::assert_runtime_model_matches_reference( - CorpusCase::Sde, - "runtime-native-aot", - &aot, - )?; - Ok(()) } } diff --git a/tests/support/bimodal_ke.rs b/tests/support/bimodal_ke.rs index 4f43d248..8d782f8f 100644 --- a/tests/support/bimodal_ke.rs +++ b/tests/support/bimodal_ke.rs @@ -2,10 +2,8 @@ use std::error::Error; use std::io; -use std::path::PathBuf; use pharmsol::prelude::*; -use tempfile::{tempdir, TempDir}; pub const MODEL_NAME: &str = "bimodal_ke"; pub const OBSERVATION_TIMES: [f64; 7] = [0.5, 1.0, 2.0, 3.0, 4.0, 6.0, 8.0]; @@ -26,27 +24,6 @@ dx(central) = -ke * central out(cp) = central / v ~ continuous() "#; -#[derive(Debug)] -pub struct ArtifactWorkspace { - tempdir: TempDir, -} - -impl ArtifactWorkspace { - pub fn new() -> Result> { - Ok(Self { - tempdir: tempdir()?, - }) - } - - pub fn aot_output(&self, stem: &str) -> PathBuf { - self.tempdir.path().join(format!("{stem}.pkm")) - } - - pub fn build_root(&self, stem: &str) -> PathBuf { - self.tempdir.path().join(stem) - } -} - fn subject_for_indices(route_index: usize, output_index: usize) -> Subject { let mut builder = Subject::builder(MODEL_NAME).infusion(0.0, 500.0, route_index, 0.5); for time in OBSERVATION_TIMES { @@ -67,10 +44,7 @@ pub fn subject() -> Subject { subject_for_labels("iv", "cp") } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] pub fn subject_for_runtime_model(model: &pharmsol::dsl::CompiledRuntimeModel) -> Subject { let route_label = if model.info().routes.iter().any(|route| route.name == "iv") { "iv" @@ -184,10 +158,7 @@ pub fn report_subject_predictions( report_values(label, &values, tolerance) } -#[cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#[cfg(feature = "dsl-jit")] pub fn report_runtime_model( label: &str, model: &pharmsol::dsl::CompiledRuntimeModel, @@ -212,38 +183,3 @@ pub fn compile_runtime_jit_model() -> Result Result> { - Ok(pharmsol::dsl::compile_module_source_to_runtime( - AUTHORING_DSL, - Some(MODEL_NAME), - pharmsol::dsl::RuntimeCompilationTarget::NativeAot( - pharmsol::dsl::NativeAotCompileOptions::new( - workspace.build_root("runtime-native-aot-build"), - ) - .with_output(workspace.aot_output("bimodal-ke-runtime-native-aot")), - ), - |_, _| {}, - )?) -} - -#[cfg(all(feature = "dsl-aot", feature = "dsl-aot-load"))] -pub fn compile_direct_aot_model( - workspace: &ArtifactWorkspace, -) -> Result> { - let artifact = pharmsol::dsl::compile_module_source_to_aot( - AUTHORING_DSL, - Some(MODEL_NAME), - pharmsol::dsl::NativeAotCompileOptions::new(workspace.build_root("direct-aot-build")) - .with_output(workspace.aot_output("bimodal-ke-direct-aot")), - |_, _| {}, - )?; - - Ok(pharmsol::dsl::load_runtime_artifact( - &artifact, - pharmsol::dsl::RuntimeArtifactFormat::NativeAot, - )?) -} diff --git a/tests/support/runtime_corpus.rs b/tests/support/runtime_corpus.rs index cab5868e..f7b98d06 100644 --- a/tests/support/runtime_corpus.rs +++ b/tests/support/runtime_corpus.rs @@ -1,12 +1,8 @@ #![allow(dead_code)] -#![cfg(any( - feature = "dsl-jit", - all(feature = "dsl-aot", feature = "dsl-aot-load") -))] +#![cfg(feature = "dsl-jit")] use std::error::Error; use std::io; -use std::path::PathBuf; use diffsol::Vector; use ndarray::Array2; @@ -17,7 +13,6 @@ use pharmsol::prelude::{ use pharmsol::{ equation, fa, fetch_cov, fetch_params, lag, Parameters, Subject, SubjectBuilderExt, SDE, }; -use tempfile::{tempdir, TempDir}; const ODE_SOURCE: &str = r#" name = one_cmt_oral_iv @@ -388,27 +383,6 @@ impl CorpusCase { } } -#[derive(Debug)] -pub struct ArtifactWorkspace { - tempdir: TempDir, -} - -impl ArtifactWorkspace { - pub fn new() -> Result> { - Ok(Self { - tempdir: tempdir()?, - }) - } - - fn aot_output(&self, stem: &str) -> PathBuf { - self.tempdir.path().join(format!("{stem}.pkm")) - } - - fn build_root(&self, stem: &str) -> PathBuf { - self.tempdir.path().join(stem) - } -} - enum ExpectedPredictions { Subject(SubjectPredictions), Particles(Array2), @@ -436,27 +410,6 @@ pub fn compile_runtime_jit_model(case: CorpusCase) -> Result Result> { - Ok(adjust_runtime_model( - case, - dsl::compile_module_source_to_runtime( - case.source(), - Some(case.model_name()), - RuntimeCompilationTarget::NativeAot( - dsl::NativeAotCompileOptions::new( - workspace.build_root(&format!("{}-runtime-aot-build", case.label())), - ) - .with_output(workspace.aot_output(&format!("{}-runtime-aot", case.label()))), - ), - |_, _| {}, - )?, - )) -} - pub fn assert_runtime_model_matches_reference( case: CorpusCase, backend_label: &str, @@ -484,36 +437,6 @@ pub fn assert_runtime_model_matches_reference( } } -pub fn assert_runtime_models_match_each_other( - case: CorpusCase, - left_label: &str, - left: &CompiledRuntimeModel, - right_label: &str, - right: &CompiledRuntimeModel, -) -> Result<(), Box> { - let left_predictions = estimate_runtime_predictions(case, left)?; - let right_predictions = estimate_runtime_predictions(case, right)?; - - match (&left_predictions, &right_predictions) { - (RuntimePredictions::Subject(left), RuntimePredictions::Subject(right)) => { - compare_subject_predictions_pairwise(case, left_label, left, right_label, right) - } - (RuntimePredictions::Particles(left), RuntimePredictions::Particles(right)) => { - compare_particle_predictions_pairwise(case, left_label, left, right_label, right) - } - (RuntimePredictions::Subject(_), RuntimePredictions::Particles(_)) - | (RuntimePredictions::Particles(_), RuntimePredictions::Subject(_)) => { - Err(io::Error::other(format!( - "{} [{} vs {}]: runtime prediction kind mismatch", - case.label(), - left_label, - right_label - )) - .into()) - } - } -} - pub fn estimate_runtime_predictions( case: CorpusCase, model: &CompiledRuntimeModel, @@ -590,51 +513,6 @@ fn compare_subject_predictions( Ok(()) } -fn compare_subject_predictions_pairwise( - case: CorpusCase, - left_label: &str, - left: &SubjectPredictions, - right_label: &str, - right: &SubjectPredictions, -) -> Result<(), Box> { - let left_values = left.flat_predictions(); - let right_values = right.flat_predictions(); - - if left_values.len() != right_values.len() { - return Err(io::Error::other(format!( - "{} [{} vs {}]: prediction length mismatch ({} vs {})", - case.label(), - left_label, - right_label, - left_values.len(), - right_values.len() - )) - .into()); - } - - for (index, (left_value, right_value)) in - left_values.iter().zip(right_values.iter()).enumerate() - { - let abs_diff = (left_value - right_value).abs(); - if abs_diff > case.tolerance() { - return Err(io::Error::other(format!( - "{} [{} vs {}]: prediction {} differed by {:.6} (left {:.6}, right {:.6}, tolerance {:.6})", - case.label(), - left_label, - right_label, - index, - abs_diff, - left_value, - right_value, - case.tolerance() - )) - .into()); - } - } - - Ok(()) -} - fn compare_particle_predictions( case: CorpusCase, backend_label: &str, @@ -676,49 +554,6 @@ fn compare_particle_predictions( Ok(()) } -fn compare_particle_predictions_pairwise( - case: CorpusCase, - left_label: &str, - left: &Array2, - right_label: &str, - right: &Array2, -) -> Result<(), Box> { - if left.dim() != right.dim() { - return Err(io::Error::other(format!( - "{} [{} vs {}]: particle matrix mismatch {:?} vs {:?}", - case.label(), - left_label, - right_label, - left.dim(), - right.dim() - )) - .into()); - } - - for row in 0..left.nrows() { - for col in 0..left.ncols() { - let left_prediction = &left[(row, col)]; - let right_prediction = &right[(row, col)]; - let abs_diff = (left_prediction.prediction() - right_prediction.prediction()).abs(); - if abs_diff > case.tolerance() { - return Err(io::Error::other(format!( - "{} [{} vs {}]: particle ({row}, {col}) differed by {:.6} (left {:.6}, right {:.6}, tolerance {:.6})", - case.label(), - left_label, - right_label, - abs_diff, - left_prediction.prediction(), - right_prediction.prediction(), - case.tolerance() - )) - .into()); - } - } - } - - Ok(()) -} - fn reference_ode_predictions() -> Result> { let model = equation::ODE::new( |x, p, t, dx, bolus, rateiv, cov| {