Page
1
(This page has no text content)
Page
2
R U S T F O R S C I E N T I F I C C O M P U T I N G Numerical Methods, Simulations, and Linear Algebra Ethan Crossley Reactive Publishing
Page
3
CONTENTS Title Page Copyright © 2026 Reactive Publishing. All Rights Reserved. Chapter 1: The Evolution of Scientific Computing Languages Chapter 2: Fundamental Rust Programming Concepts Chapter 3: Performance Optimization in Rust Chapter 4: Numerical Libraries in Rust Chapter 5: Linear Algebra in Rust Chapter 6: Solving Differential Equations Chapter 7: High-Performance Simulations with Rust Chapter 8: Advanced Data Handling and Processing Chapter 9: Numerical Methods and Algorithms Chapter 10: Functional Programming Paradigms in Rust Chapter 11: Interfacing Rust with Other Languages Chapter 12: Testing, Debugging, and Maintenance Chapter 13: Building User Interfaces for Scientific Tools Chapter 14: Parallel and Distributed Computing Chapter 15: Security Considerations in Scientific Computing Chapter 16: Machine Learning and Data Analysis in Rust Chapter 17: Scientific Application Case Studies
Page
4
COPYRIGHT © 2026 REACTIVE PUBLISHING. ALL RIGHTS RESERVED. All rights reserved. No portion of this publication may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, whether electronic, mechanical, photocopying, recording, or otherwise, without the prior written consent of the publisher, except for brief quotations used in reviews or scholarly articles. Published by Reactive Publishing. The content of this book is provided solely for educational and informational purposes. The author and publisher make no representations or warranties regarding the accuracy, completeness, or applicability of the information contained herein and disclaim any liability arising from its use. For copyright inquiries or permissions, please visit www.reactivepublishing.org Email: support@reactivepublishing.org
Page
5
S CHAPTER 1: THE EVOLUTION OF SCIENTIFIC COMPUTING LANGUAGES cientific computing carries a genealogy you can trace from punch-card rooms to GPU clusters, and every language choice along that arc narrates priorities, crises, and stubborn institutions. Early decisions were ruthlessly pragmatic: Fortran was born to tame numerical loops on scarce hardware, to squeeze throughput from vacuum tubes and transistor boards, to make scientists’ results reproducible when a flakey run could invalidate months of lab work. Those trade-offs produced durable artifacts, ocean models, climate ensembles, linear algebra libraries, that still underpin critical research, as if yesterday’s constraints embossed today’s trust. That durability breeds a strange inertia: software that solves real problems becomes a de facto standard even when the surrounding world changes. The same code that preserves peer-reviewed models also traps entire fields on design patterns written before modern compiler analyses, memory safety, and parallel abstractions. Replace a million-line, battle-tested Fortran model and you risk subtle numerical drift; keep it and you accept brittle parallelism and escalating maintenance debt. The dilemma is quiet but relentless.
Page
6
There is a paradox at the heart of progress: older languages are both the most trusted and the most fragile. Trust accrues through longevity and verification; fragility springs from legacy assumptions about memory layout, typing, and execution that modern hardware routinely violates. Solving today’s problems therefore requires two contradictory instincts at once, conservative respect for validated results and a radical demand for language guarantees, so that speed coexists with safety, determinism with concurrency, and reproducibility with hardware heterogeneity. A weather-center moment makes this visceral. A senior scientist slid a Fortran subroutine across a cluttered desk, responsible for advection in operational forecasts, and warned, “If you touch this and introduce even the smallest drift, we’ll notice it in three days of forecasts.” A young engineer rewrote that routine in a modern language with explicit contracts and memory checks; the deterministic forecast matched, but when scaled on the cluster the rewrite exposed latent race conditions. Modernization delivered confidence and simultaneously detonated hidden technical debt, proof that stability can be a house built over unexamined faults. Compare a tight numerical loop across eras and the priorities are obvious. ! Fortran do i = 1, n a(i) = b(i) + c(i) * dt end do // Rust for i in 0..n { a[i] = b[i] + c[i] * dt; } Fortran’s terse expressiveness assumes a trusting runtime; Rust’s explicit indexing and ownership declare intent to the compiler and enforce invariants at compile time. That enforcement eliminates a class of silent memory corruptions that have wrecked long simulations. The cost is design complexity, not runtime tax: modern compilers and optimized builds can elide bounds checks when provably unnecessary, producing performance that competes with, often matches, the old guard.
Page
7
Language change is not merely syntactic; it’s an ecosystem migration from monolithic compilers and bespoke libraries to modular crates, package managers, and small testable components. That cultural shift amplifies reuse and reproducibility: pin dependency versions, reproduce builds across machines, and run automated tests that exercise numerical edge cases. Tooling now speaks as loudly as syntax; the difference between a one-off script and a vetted pipeline is often a few dozen CI jobs and a lockfile. Two forces shaped the last decade: exploding hardware diversity and an uncompromising demand for correctness. Multicore CPUs, manycore accelerators, and distributed environments force new concurrency models; GPU offload requires explicit memory layout and transfer semantics. At the same time, reproducibility and provenance initiatives have made silent numerical flips intolerable. Languages that combine explicit resource control, strong typing, and a clear path to low-level optimization sit uniquely well between raw silicon and verified science. Performance without safety is reckless; safety without performance is irrelevant. Adoption rarely follows a purely technical line. The calculus includes porting validated models, the availability of domain libraries, and institutional appetite to retrain staff. Pragmatic migrations are emerging: FFI bindings, thin interoperability layers, and surgical rewrites that isolate performance-critical kernels while preserving validated high-level logic. The hybrid approach reduces risk and yields concrete payoffs, safer threading, clearer buffer ownership, and auditable numerical kernels. The arc from assembly and FORTRAN through MATLAB and Python to modern compiled languages marks a shifting balance between immediacy and guarantees. Early languages handed scientists the ability to compute; newer languages hand them the ability to compute reliably at scale. The question is not whether change is possible but how to reconcile auditability with speed, safety with expressiveness; languages that emphasize ownership, compile-time checks, and performance engineering point to a practical answer. The future of high-performance science will be written where trust collides with modern guarantees, and that collision is where better science begins. Why Rust? Safety and Performance
Page
8
Rust solves a problem scientists have lived with for decades, getting C-like speed without C-like surprises. Picture a graph laid across every line of numerical code: one axis for correctness, the other for raw throughput. Most languages make them a trade-off; Rust redraws the map and refuses that compromise. It encodes invariants into the compilation process so the runtime can be lean and predictable, not taxed by a hidden garbage collector or by pointer tricks you cannot inspect. The consequence is not just fewer bugs; it is predictable, measurable operational reliability that shows up as shorter debugging marathons and reproducible experiments you can trust. The language achieves that through deliberate, visible mechanisms: a strict type system that refuses sloppy intent, move semantics that make ownership explicit, and a borrow checker that turns whole classes of memory errors into compiler errors. Those errors are not bureaucratic hurdles; they are early warnings that save human-hours downstream. When a system is designed so mistakes are corrected before they run, your logs stop being a morgue and become a ledger of meaningful performance. Memory safety is enforced by explicit ownership and borrowing rules that eliminate dangling pointers and double frees at the language level, and concurrency safety is baked into types: the Send and Sync traits express what can cross thread boundaries, and the compiler rejects data races before any thread wakes. Performance comes from complementary choices, zero- cost abstractions, monomorphized generics, predictable memory layout, aggressive LLVM optimizations, and the deliberate option to drop into unsafe code for micro-optimizations. Legacy toolchains often force you to choose between speed and correctness; Rust asks you to state your intent and then enforces it without a hidden runtime tax. A concrete kernel crystallizes the point: one version written in idiomatic safe Rust, another using an unsafe pointer walk for hand-tuned iteration. With optimizations enabled, compilers frequently emit identical assembly for both. // safe, idiomatic fn saxpy_safe(a: &mut [f64], b: &[f64], c: f64) { for (ai, &bi) in a.iter_mut().zip(b.iter()) {
Page
9
*ai += bi * c; } } // unsafe manual walk (same semantics) unsafe fn saxpy_unsafe(a: *mut f64, b: *const f64, n: usize, c: f64) { for i in 0..n { let ai = a.add(i); let bi = b.add(i); *ai = *ai + *bi * c; } } Safety is not the opposite of speed; it is its substrate. There is a cognitive flip at Rust’s core: you pay complexity at compile time to avoid existential risks at runtime. That sounds like a tax, and sometimes the borrow checker will force you to rethink an algorithm, but the payoff is paradoxical and large: non-deterministic crashes, corrupted arrays, and subtle race conditions that once consumed weeks become impossible or trivial to fix. A lab I know spent three months chasing a nondeterministic failure in a Monte Carlo pipeline; when they ported the kernel to Rust, compilation produced a single error that exposed a use-after-free in a C extension. They recovered months of calendar time and, far more valuable, trust in nightly ensembles. Concurrency becomes not a minefield but a lever. Immutable data is trivially sharable; mutable data must be uniquely owned or explicitly guarded by synchronization primitives whose invariants the compiler can validate. Libraries such as Rayon map data-parallel patterns to threads while preserving these guarantees, letting teams scale concurrency without the usual proportional explosion of debugging headaches.
Page
10
When you really need the last cycle, Rust negotiates rather than hides choices. The unsafe keyword is explicit and local, an auditable beacon that allows pointer arithmetic, raw memory access, and CPU intrinsics where necessary. Because unsafe is opt-in and visible, tests and formal verification can concentrate on small, high-risk islands, while the rest of the code remains in the compiler’s safety envelope. Performance engineering in Rust will look familiar to seasoned engineers, but cleaner: profile, isolate hotspots, optimize in release with LTO and target-specific codegen, and leverage SIMD via portable_simd or intrinsics when the profiler points. Prefer slice-based algorithms and iterator patterns the optimizer understands. Use Criterion or cargo-bench to measure microseconds with confidence; when Rust code regresses, the error is usually a real regression, not an undefined-behavior illusion. Adoption is surgical, not revolutionary: extract the numerical kernels, express clear ownership, expose a minimal C-compatible API, and integrate with existing pipelines. This hybrid approach preserves validated high-level logic while hardening inner loops. The tactical wins are immediate, faster experiments, more reliable cluster runs, and the strategic returns compound through maintainability and reproducibility. Choosing Rust is both technical and cultural: it rewards upfront precision and pays dividends in reproducibility, maintainability, and predictable performance. For simulations that run for days on shared clusters or for models that influence policy, the greater guarantor is not raw throughput but the certainty that results are reproducible. Much of the time the performance gains are modest; sometimes they are dramatic, but they are dependable because the language makes whole classes of bugs literally unrepresentable. Overview of Rust’s Ownership Model Ownership is the spine of Rust’s guarantees. Every value has a single owner, and the compiler enforces how that ownership moves, borrows, and expires. Picture a laboratory where each instrument must be checked out by one scientist at a time and an immutable ledger records every handoff; there are no phantom users, no midnight surprises. In Rust that ledger is implicit and checked at compile time. A String owns its buffer; passing it by value hands
Page
11
over the key. By forcing ownership to be explicit, entire classes of late- night runtime disasters, use-after-free, double-free, dangling pointers, are erased before the program ever runs, freeing you to reason about system behavior long before you debug corruption at 2 a.m. Move semantics are the practical axis of this model: assign or pass a non- Copy type by value and ownership transfers. The rule is tiny and brutal and beautifully predictive. fn main() { let a = String::from("data"); let b = a; // ownership moves from a to b // println!("{}", a); // compile-time error: borrow of moved value println!("{}", b); } That compile-time refusal feels strict until you realize it encodes intent: either deliberately clone the buffer or accept a single manager for that memory. Cloning is explicit and deliberately expensive, which prevents accidental gigabyte copies in numerical pipelines and makes cost visible in the code rather than hiding it behind silent performance regressions. Borrowing is the second pillar: lend access without surrendering control, and the compiler tracks precisely how long that loan lasts. Immutable borrows are plentiful and cheap; mutable borrows are exclusive and narrowly scoped. The rule is simple: any number of immutable borrows or exactly one mutable borrow at a time. That single constraint eliminates whole categories of data races at the source. fn scale(slice: &mut [f64], factor: f64) { for v in slice.iter_mut() { *v *= factor; } }
Page
12
fn main() { let mut data = vec![1.0, 2.0, 3.0]; scale(&mut data, 0.5); println!("{:?}", data); } Lifetimes are the glue that pins references to the scopes that validate them. Most lifetimes are inferred and invisible, but the compiler will speak up when you try to return a reference tied to a local variable, because that would create a pointer to freed memory. Rust’s rejection in those cases is not obstinacy; it is protection. fn invalid<'a>() -> &'a String { let s = String::from("local"); &s // error: `s` does not live long enough } Deterministic destruction through Drop makes resource management visceral: files, sockets, GPU buffers, and mapped memory are released the moment their owner goes out of scope. There are no GC pauses, no finalizers that run at random times, just a predictable destructor and an auditable lifecycle. For scientific workloads that juggle large buffers and device memory, that predictability is currency. Unsafe is an explicit gate where you can perform pointer arithmetic, raw memory access, and call into FFI; stepping through that gate is an agreement to uphold invariants the compiler cannot prove. The brilliance is that unsafe is visible and local: audits, tests, and formal proofs can focus on small hotspots rather than guarding a sprawling, implicit surface of danger. Small, reviewed islands of unsafety are far easier to defend than a sea of quietly assumed correctness. Concurrency integrates with ownership through traits that are compile-time promises. Send marks types that can move between threads; Sync marks types whose references can be shared. These are not runtime flags but type-
Page
13
system contracts. If your type embeds non-thread-safe raw pointers or unsynchronized interior mutability, the compiler will refuse to let it cross thread boundaries until you wrap it in a safe primitive or redesign it. The type system forces the conversation about thread safety into code, where it can be read, audited, and tested. There is a paradox: adding constraints simplifies reasoning. Engineers fear stricter rules because rules sound like friction, yet the constraints create predictable boundaries for change. “The compiler is not your adversary; it is an ally that refuses to recognize impossible promises.” A computational biologist once rewired a parallel preprocessing pipeline to Rust and expected a long hunt for a data race; instead the compiler flagged a function trying to hand the same buffer to two threads, she fixed the ownership, recompiled, and the multi-core run succeeded on the first try. The surprise was how fast trust arrived. Practical patterns emerge naturally. Design your APIs to take references for zero-copy reads and to take ownership when the callee should manage lifecycle. Use slices and iterators to express contiguous memory without moving large buffers. Keep unsafe interactions localized and well-tested. Favor composition of small owners over global mutable state; the compiler rewards modularity with safety and clarity. When bridging to C libraries or exposing C-compatible APIs, treat ownership as the contract across the boundary: who allocates, who frees, and when. Convert raw pointers into owned Rust types as soon as possible so the rest of your codebase benefits from the guarantees that ownership provides. Write tiny kernels that move big arrays, borrow slices for computation, and deliberately provoke the borrow checker until its messages become familiar. Compile those experiments repeatedly. The guarantees will stop being abstract rules and become a tactile part of your workflow, the kind of safety that lets you sleep and lets your simulations scale. Setting Up Your Rust Environment Installing Rust correctly is the first reproducible act that turns ideas into fast, auditable experiments.
Page
14
A reproducible environment is the shortest path from idea to publication.” Say it on a slide and people will nod; live it and reviewers stop asking for the Dockerfile. Rustup, the official toolchain installer and manager, becomes your conductor: it balances stable, beta, and nightly channels, adds cross‐compilation targets, and keeps toolchain changes isolated per project so experiments remain repeatable rather than ephemeral. On macOS and Linux a single, auditable install sequence gets you to a baseline you can share with a collaborator or pin in CI: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh source $HOME/.cargo/env rustup default stable rustc --version cargo --version Windows asks for a small, deliberate choice: MSVC or GNU toolchain. Choose MSVC when you must link against native libraries built with Visual Studio; choose GNU for POSIX-like behavior under MSYS2 or WSL. That choice is not academic, one wrong selection can cost you a day rebuilding BLAS stacks and chasing ABI mismatches, so decide deliberately and document it. Tooling beyond rustc is non-negotiable for professional work. Install formatting, linting, and convenience commands into the toolchain rather than scattering binaries on PATH: rustup component add rustfmt clippy cargo install cargo-edit Scientific Rust rarely stands alone; C and Fortran dependencies must compile cleanly. On Debian/Ubuntu: sudo apt update sudo apt install build-essential pkg-config libopenblas-dev liblapack-dev On macOS, use Homebrew:
Page
15
brew install openblas pkg-config When you link against optimized BLAS (OpenBLAS, MKL), install those libraries via your package manager and prefer crates such as blas-src or intel-mkl-src, or vendor-specific bindings; pkg-config will usually provide correct link flags. Think of the linker as an unforgiving gatekeeper: silent when all is well, merciless when symbols are missing. Performance-tuning flags belong in Cargo.toml where they are version- controlled and visible to collaborators, not whispered into the shell. A practical release profile for numerical code: [profile.release] opt-level = "z" # set to "3" for maximum speed lto = true codegen-units = 1 debug = false For highest single-node throughput, set opt-level = “3” and target the native CPU: export RUSTFLAGS="-C target-cpu=native Bake these flags into CI for reproducibility. Paradox: the fastest code often begins with the slowest install, spend the time fixing flags and libraries now, and you save weeks of head-scratching later. Editor integration turns Rust from a command-line curiosity into an everyday productivity engine. Visual Studio Code with rust-analyzer provides instant type information, inline diagnostics, and refactor support; configure rust-analyzer to follow workspace settings and to honor the project’s rust-toolchain file so everyone sees the same toolchain. Add provenance files to the repository to pin behavior and expectations: rust-toolchain rustfmt.toml .github/workflows/ci.yml
Page
16
Benchmarks and tests are part of the scientific method. Use Criterion for statistically robust microbenchmarks and limit cargo bench runs in CI to performance branches to avoid flakey pipelines: [dev-dependencies] criterion = "0.4 Record inputs, random seeds, and environmental metadata alongside results so later regressions become forensic artifacts, not mysteries. Targeting GPUs or WebAssembly requires explicit targets and sometimes nightly toolchains. Add wasm32-unknown-unknown with rustup when building for the web; CUDA workflows often need nightly Rust and specialized crates or toolchains, document every deviation in README and lock it in rust-toolchain. The single most common reproducibility failure is “it works on my laptop”: lock your choices and spare future you the apology note. A postdoc once shipped a promising climate kernel to a colleague on Windows; compilation succeeded but runtime crashed because the native OpenBLAS ABI differed. They containerized the build, pinned the ABI, and the kernel ran everywhere. The human cost there was not lost time but a fractured trust in the build process, pin toolchains and system libraries together; reproducibility is a social contract with your future self. When things break, the clues live in environment variables and system packages. If Cargo cannot find a library, inspect PKG_CONFIG_PATH and LD_LIBRARY_PATH (macOS: DYLD_FALLBACK_LIBRARY_PATH). On Windows ensure Visual Studio Build Tools are installed and that cl.exe is visible to your shell. For many Windows users, WSL provides the path of least resistance to POSIX toolchains and native OpenBLAS. Let linker error messages be your guide: they are noisy, precise, and fixable. Organize multi-crate scientific pipelines with Cargo workspaces to share dependencies, unify profiles, and simplify CI: [workspace] members = ["core", "benchmarks", "cli"]
Page
17
Workspaces reduce duplication and turn a jungle of Cargo.toml files into a single source of truth for reproducible builds. Security and dependency hygiene matter as much as performance. Run cargo-audit to scan for vulnerabilities, commit Cargo.lock for applications (but not libraries), and only run cargo update intentionally, treat upgrades like experiments that require verification. A locked dependency tree is not stagnation; it’s a documented baseline you can compare regressions against. Create a tiny numerical program and run it. The choices you made, toolchain, system libraries, profile flags, editor configuration, will decide whether that first run is a quiet success or a multi-day chase. When compiler, editor, and linker are configured and committed, executing a Rust numerical routine becomes a deterministic, replicable experiment instead of a leap of faith. First Steps: Your First Rust Program Write your first Rust program and you have crossed the line where idea becomes verifiable experiment: a binary you can hand to a collaborator and expect the same numbers back. Cargo scaffolds that boundary in seconds. At your terminal: cargo new --bin first-rust-numerics cd first-rust-numerics Cargo produces a tidy layout , Cargo.toml, src/main.rs , and a single command ties editing, building, and packaging together. The simplicity is deceptive: inside those few files live the knobs that make scientific computing reproducible, auditable, and fast. A more useful first program than “Hello, world” computes a small numeric quantity with explicit types and a test. Drop this into src/main.rs and run it: fn mean(xs: &[f64]) -> f64 { let n = xs.len() as f64; xs.iter().copied().sum::<f64>() / n }
Page
18
fn std_dev(xs: &[f64]) -> f64 { let m = mean(xs); let var = xs.iter().map(|x| (x - m).powi(2)).sum::<f64>() / (xs.len() as f64); var.sqrt() } fn main() { let values = vec![1.0, 2.0, 3.0, 4.0, 5.0]; println!("mean = {:.6}, std_dev = {:.6}", mean(&values), std_dev(&values)); } Pause on a few tiny choices: the f64 annotations and the copied() call are small decisions with outsized consequences. They declare intent (double precision), avoid accidental integer division, and sidestep borrow friction. The mean function shows how iterator adapters compose concisely , no manual loops, no hidden allocations , a tangible example of zero-cost abstractions in production. Build and iterate with: cargo run cargo build --release ./target/release/first-rust-numerics A paradox surfaces: debug builds are faster to write but slower to trust. The numbers you get in a quick debug run can shift subtly when you switch to release because of optimizations, inlining, or vectorization; that shift is not a bug in Rust so much as a reminder that performance tuning changes the arithmetic landscape. Tests convert that reminder into a safeguard. Tuck this unit test alongside your functions:
Page
19
mod tests { use super::*; fn mean_std_of_five() { let xs = [1.0, 2.0, 3.0, 4.0, 5.0]; assert!((mean(&xs) - 3.0).abs() < 1e-12); assert!((std_dev(&xs) - 1.4142135623730951).abs() < 1e-12); } } Run cargo test and watch the harness convert one-off curiosity into repeatable checkpoints. Unit tests are cheap insurance; they make experiments auditable. A small, human misstep illustrates the stakes. A colleague left a debug build as the canonical artifact for a week-long climate kernel; development runs were fast, so they shipped the wrong artifact. The production run , compiled with release optimizations and vectorized loops , produced slightly different floating‐point trajectories. Weeks of chasing ghosts later the culprit was clear: associativity changes from optimization interacting with an unpinned RNG. They learned to always pin seeds, preserve test vectors, and include a reproducible recipe with every numeric claim. Embarrassment turned into discipline. Tooling matters as much as code. rustfmt and clippy are not niceties; they are reproducible style and correctness aids. Run: cargo fmt cargo clippy -- -D warnings Formatting reduces noisy diffs; lints flag numerical pitfalls , unused mutability, hidden casts, or comparisons that ignore NaN semantics. Treat clippy warnings as experiments: some are stylistic, some catch logic that would silently corrupt results.
Page
20
A running binary is the only convincing experiment.” Source code is persuasive theory; the binary, test outputs, and the exact toolchain versions are the evidence reviewers, collaborators, and your future self will demand. Performance knobs live where you expect them: Cargo profiles, RUSTFLAGS, and -C options. For heavier kernels add profile sections to Cargo.toml and consider -C target-cpu=native for local tuning. Never confuse speed with correctness. Microbenchmarks mislead unless you pin compiler flags, control for warm-up, and sample over statistically significant runs. When you add crates , RNGs, BLAS bindings, plotting libraries , remember each dependency brings ABI shape and numerical behavior you must document. Pin RNG seeds, commit Cargo.lock, and record crate versions so others can reproduce your exact trajectory. The first program teaches a workflow more than a language. Create the project, write a deterministic numeric routine, add tests, format and lint, build release, and record the exact command you used. Wrap those steps into a tiny run.sh so the experiment is literally one command away. That tiny loop , write, test, build, record , scales. The type choices and iterator idioms you used when implementing mean and std_dev will shape how you structure simulations, manage memory, and assemble numerical kernels; those small choices ripple through performance and safety in ways you will come to appreciate by measuring them. Key Features Relevant to Scientific Work Tiny errors magnify, algorithms scale nonlinearly, and a misplaced pointer can turn weeks of careful work into someone’s cautionary tale. Naming numeric precision up front forces clarity. Rust makes you pick f32 or f64, i32 or u64, and it refuses the silent casts that hide truncations and rounding screams. That actually saves experiments: an explicit conversion is a deliberate decision, not an accident. let count: usize = data.len(); let mean = sum as f64 / count as f64; // explicit, unambiguous Deciding precision at declaration exposes the performance–accuracy trade- offs where you can see them, not where they leak into a late-night