Arity: a fast R language server, formatter, and linter written in Rust
▲ 78 r/rstats

Arity: a fast R language server, formatter, and linter written in Rust

I'm happy to announce Arity, an all-in-one toolchain for R: a language server, formatter, and linter, built in Rust on top of a lossless, incremental parser (the same rust-analyzer architecture: rowan for syntax trees, salsa for incremental reparsing).

The goal is a fast, deterministic development experience that just works in your editor. Here are some of the the most important features:

  • Formatter: deterministic, rule-based formatting toward the tidyverse style guide. Output is idempotent and doesn't depend on your existing line breaks. roxygen comments are handled too.
  • Linter: a growing set of correctness, readability, and performance rules, many with safe autofixes.
  • Language server: formatting, diagnostics with quick fixes, hover, completion, signature help, go-to-definition and references, rename, document and workspace symbols, semantic tokens, folding, and call hierarchy.

Arity runs on Linux, macOS, and Windows (x86_64 and arm64). You can install it in several ways:

  • cargo install arity
  • npm install -g arity-cli (bundles a prebuilt binary, no Rust toolchain needed)
  • uv tool install arity or pipx install arity
  • VS Code and Open VSX extension (also works in Positron)
  • Prebuilt binaries from the releases page

Neovim, Helix, and other editors are covered in the editor setup guide.

Acknowledgements

Arity is heavily inspired by air (Posit's R formatter), and borrows tests, rules, and style from it, as well as from jarl. The big difference from air is that Arity also aims to be a full linter and language server, not just a formatter.

It's still early (v0.8.0), so expect rough edges and please file issues if you encounter them. The docs are at arity.cc and the source is at github.com/jolars/arity.

u/johlars — 4 days ago
▲ 26 r/rust

Eunoia: Area-Proportional Euler/Venn Diagrams in Rust

Area-proportional three-set Euler diagram drawn with ellipses in a pinwheel, set names and counts centered inside each lobe, the four small pairwise/triple intersection counts pulled out to the side with leader lines

I've been building Eunoia, a Rust library for area-proportional Euler and Venn diagrams, which is the kind where every circle/ellipse and every overlap is sized in proportion to the data.

It's a ground-up Rust rewrite of eulerr, my R package. The interesting part is that drawing these well is barely a graphics problem and instead primarily a nonlinear optimization problem. You're searching for shape positions and sizes that make every region's area match its target, which means a real fitting pipeline: you need an initial optimization strategy, a follow-up tuning strategy, and global optimization fallbacks to escape local minima.

The API keeps the spec (what to draw) separate from the shape choice (made at fit time, as a generic parameter):

use eunoia::{DiagramSpecBuilder, Fitter, InputType};
use eunoia::geometry::shapes::Ellipse;

// The eulerAPE three-set example (the diagram above).
let spec = DiagramSpecBuilder::new()
    .set("a", 3491.0)
    .set("b", 3409.0)
    .set("c", 3503.0)
    .intersection(&["a", "b"], 120.0)
    .intersection(&["a", "c"], 114.0)
    .intersection(&["b", "c"], 132.0)
    .intersection(&["a", "b", "c"], 126.0)
    .input_type(InputType::Exclusive)
    .build()
    .unwrap();

// Shapes `Ellipse` for `Circle`, `Square`, or `Rectangle` are currently supported.
let layout = Fitter::<Ellipse>::new(&spec).seed(1).fit().unwrap();
println!("loss = {:.3e}", layout.loss());

The diagram above is that exact spec. Three circles can't represent this configuration (the triple intersection is large relative to the pairwise ones); ellipses fit it to a loss of ~1e-23 (essentially exact). The four tiny intersections have no room for their count labels, so the label-placement pass (poles of inaccessibility for the interior anchors, ray-cast leaders for the overflow) pushes them outside and draws leader lines.

Here's some features I'm happy with:

  • One pure-Rust core, many targets. The same engine compiles natively and to WebAssembly. There are bindings (npm/JS, Python on PyPI, R, and a Julia one coming), but the Rust crate is the backbone of everything: eunoia on crates.io.
  • It spun off a second crate. The optimization needs pushed me to write basin, a standalone, dependency-light Rust optimization crate (Levenberg-Marquardt, L-BFGS, Nelder-Mead, CMA-ES), now Eunoia's sole optimizer dependency. See https://basin.bz for quick docs and examples.
  • Many shapes. eulerr only supported circles and ellipses before, but with the rewrite, I added squares and rectangles.
  • Efficiency. eulerr used numerical gradients for the final optimization stage. But Eunoia implements analytical gradients for all shapes and smooth loss functions, which results in significant speedups.
  • Optimization strategies. The crate makes several fitting strategies available. And an especially wanted addition (compared to eulerr) is Levendberg-Marquardt, which turns out to work incredibly well for the default sums-of-squares loss.
  • WASM bindings. The crate is WASM-compatible by default, which allowed me to retire the old Shiny-based web app that relied on eulerr.

Links

Please let me know if you have any questions. Feedback and contributions are highly welcome!

reddit.com
u/johlars — 23 days ago
▲ 134 r/ScientificComputing+2 crossposts

Announcing Basin: A Numerical Optimization Library for Rust

Hi!

I've been working on Basin, a numerical optimization library for Rust. It's heavily inspired by argmin; same overall shape (Executor driver loop, Solver/Problem trait split, per-solver State, pluggable termination). Basin exists because I wanted to push on a few specific design directions that were awkward to retrofit.

What's Different

  • Framework-level termination, bound to state shape. max_iter, the *_tolerance family, max_time, eval budgets all live on the Executor and compose across solvers. Each criterion binds on the minimum state it needs, so asking for a GradientTolerance on a derivative-free solver is a compile error, not a runtime surprise.
  • First-class constraints. Constraints describe the problem, so they live problem-side (not on the executor, never on state). A constrained problem handed to an unconstrained solver is a compile error; there are opt-in adapters (projection/log-barrier/augmented Lagrangian) to wrap unconstrained solvers. Box bounds and linear (in)equalities today; nonlinear is on the roadmap.
  • Backend-generic linear algebra. Solvers are generic over Vec<f64> (no features), nalgebra, ndarray, and faer. A small universal vector tier keeps first-order/derivative-free solvers backend-generic; a richer linalg tier holds matrix ops that LA-heavy solvers bound on by the minimum subset they need.
  • WASM in the default build. No BLAS/LAPACK, no threads, no std::time::Instant in default paths. CI enforces wasm32-unknown-unknown. Parallelism and BLAS-backed paths are opt-in features.

Current Solvers Supported

  • First-order/quasi-Newton: gradient descent (with momentum + pluggable line searches), BFGS, L-BFGS, L-BFGS-B
  • Derivative-free: Nelder--Mead, Brent
  • Nonlinear least squares: Gauss--Newton, Levenberg--Marquardt, trust-region-reflective
  • Global/stochastic: random search, CMA-ES, steady-state GA, memetic combinations
  • Constrained: projected gradient, bounded Nelder--Mead/L-BFGS-B/CMA-ES, log-barrier, augmented Lagrangian

Example

use basin::{BasicState, CostFunction, Executor, Gradient, GradientDescent, GradientTolerance};

struct Rosenbrock;

impl CostFunction for Rosenbrock {
    type Param = Vec<f64>;
    type Output = f64;
    fn cost(&self, x: &Vec<f64>) -> f64 {
        (1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2)
    }
}

impl Gradient for Rosenbrock {
    type Param = Vec<f64>;
    type Gradient = Vec<f64>;
    fn gradient(&self, x: &Vec<f64>) -> Vec<f64> {
        vec![
            -2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0].powi(2)),
            200.0 * (x[1] - x[0].powi(2)),
        ]
    }
}

let result = Executor::new(Rosenbrock, GradientDescent::new(1e-3), BasicState::new(vec![-1.2, 1.0]))
    .max_iter(50_000)
    .terminate_on(GradientTolerance(1e-6))
    .run();

Links

Feedback is very welcome, especially on the trait surface, naming, and any solver/backend combinations you'd want that aren't there yet.

u/johlars — 1 month ago