Adamantium is a Rust library and set of lint rules for writing concurrent code with copy-on-write values, scoped tasks, actors, and bounded channels. The rules restrict features such as detached tasks, locks held across await, and common sources of panics.
The cargo adamantium subcommand checks projects against these rules. It also offers a style review based on the Google Rust style guide and the OpenAI Decisions API.
Add the library to a project:
[dependencies]
adamantium = { git = "https://github.com/xipkit/adamantium.git", rev = "..." }The default tokio feature provides Task, Tasks, concurrently, the
concurrent streams, and blocking. Set default-features = false to use Actor, Str, Bytes,
Array, Deque, Map, Set, and Channel without Tokio, including on WASI. The serde
feature can be enabled independently.
Install the subcommand:
cargo install --locked --git https://github.com/xipkit/adamantium cargo-adamantiumAdamantium requires Rust 1.88 or later. This repository uses the library and follows the same rules.
cargo adamantium check # run Clippy with the rules
cargo adamantium check --format json
cargo adamantium init # write the rules into Cargo.toml and clippy.toml
cargo adamantium rules # list the rules and why each existscheck runs cargo clippy --all-targets with Adamantium's rules and clippy.toml. It disables Clippy's default lints and runs the rules as warnings, allowing crates with findings and their dependents to be checked. It checks every workspace member unless arguments after -- select packages; those arguments are passed to cargo clippy.
The command prints diagnostics and a summary to stderr. With --format json, it prints a report to stdout with the rule, message, file, line, and column for each finding. The exit status is 0 when clean, 1 for findings, and 2 when the project did not build.
The JSON check report uses schema_version: 2. Its warnings array contains warnings about Adamantium's Clippy configuration and unknown, renamed, or removed lints, each with a message, file, and line. Warnings do not change the status or exit code. Configuration warnings and errors name the file clippy.toml; their lines refer to Adamantium's bundled configuration, which cargo adamantium rules prints. Lint warnings without a primary span (E0602 and renamed_and_removed_lints) have file: null and line: null and appear once per code and message across targets. Compiler summaries are omitted. Other compiler warnings appear only in text output.
The rules also require overflow-checks = true in the release profile. Integer overflow then panics in release builds, as it does in debug builds.
init adds the rules to a project so plain cargo clippy applies them. It writes the [lints] tables, or [workspace.lints] in a workspace, the release profile's overflow check, and a clippy.toml. It excludes rules listed under allow or exceptions in the skip list. Running it again makes no changes, and it will not replace a different clippy.toml without --force.
To adopt the rules gradually, add a skip list to Cargo.toml:
[workspace.metadata.adamantium]
skip = ["tests/", "examples/"] # skip findings in these paths
allow = ["indexing_slicing", "unwrap_used"] # skip these rules throughout the project
[workspace.metadata.adamantium.exceptions]
future_not_send = ["src/syntax/", "src/bytecode.rs"] # skip this rule in these pathsFor a single package, use [package.metadata.adamantium]. Paths are globs relative to the directory containing Cargo.toml; paths without wildcards also cover their contents. Use either the bare rule name or the qualified name printed by cargo adamantium rules. Unknown rule names produce an error.
check summarizes skipped findings and reports counts by rule in the JSON report's skipped map. Remove a rule from allow to see its findings. Projects with a skip list should run cargo adamantium check in CI because a [lints] table cannot scope a rule to a path.
For a machine-readable list of rules and their reasons, run cargo adamantium rules --format json.
use adamantium::{Actor, Array, Channel, Map, Str, Tasks, blocking, concurrently};
// Clones share storage until a write makes a copy.
let items = Array::from([Str::from("a"), Str::from("b")]);
let mut more = items.clone();
more.push("c".into());
assert_eq!(items.len(), 2);
// A scope owns its tasks and waits for them to finish.
let (profile, feed) = Tasks::run(async |t| {
let profile = t.spawn(fetch("/profile"));
let feed = t.spawn(fetch("/feed"));
Ok((t.value(profile).await?, t.value(feed).await?))
})
.await?;
// Map in parallel, with a concurrency limit of eight.
let pages = concurrently(urls, 8, fetch).await?;
// Access shared state without holding a lock across an await.
let tally = Actor::new(Tally::default());
tally.with(|t| t.record(&profile));
let total = tally.read(Tally::total);
// Send and receive through a bounded channel.
let feed = Channel::<Str>::bounded(16);
// Run blocking work on the blocking pool.
let text = blocking(|| std::fs::read_to_string("notes.txt")).await?;-
Str,Bytes,Array<T>,Deque<T>,Map<K, V>, andSet<T>use copy-on-write storage, so changes to a clone do not affect the original.Str,Bytes, andArrayuseDereffor reads throughstror slice views;DequeusesDereftoVecDequeand supports pushing and popping at both ends.Maphas noIndeximplementation, and writes that take a position returnNoneor return the value instead of panicking. These types areSend + Syncwhen their element types are and support serialization with theserdefeature. -
Set<T>stores unique values in insertion order, removes values without reordering the rest, and compares membership regardless of order. Union, intersection, difference, and symmetric difference borrow values in the documented set order; collect an iterator to make an owned set. Withserde, sets serialize as sequences in insertion order and deserialize duplicate values only once. -
Tasks<E>manages a scope of tasks.spawnstarts a task, andvaluewaits for its result.joinandTasks::runwait for remaining tasks. Unobserved task errors are returned at join; if the body or an unobserved task returns an error, the remaining tasks are aborted. Task panics resume in the scope. Specify the error type withTasks::<E>::runwhen it cannot be inferred, such as when all errors pass through?. -
concurrentlymaps items in parallel with a concurrency limit and returns results in input order. It starts when first polled and takes each item only when a place opens, so at mostlimitfutures exist at once and a failure stops it taking more. The iterator and closure must beSend, and they run inside the returned future's poll. -
blockingruns a closure on the blocking pool and resumes any panic on the calling task. -
Actor<S>shares state between tasks.withprovides exclusive access andreadprovides read-only access through synchronous closures, so neither holds the lock across an await. A read-only method such asTally::totalcan be passed toread. Callingwithorreadfrom inside either method on any actor panics to prevent deadlocks. -
Channel<T>is a bounded queue whose clones can send and receive. Consume it withsplit()to get separate clonableSender<T>andReceiver<T>handles. Dropping the last sender closes the channel; receivers drain queued messages and then receiveNone. Dropping the last receiver wakes pending sends withClosed(value). Any retained combinedChannelclones still own both endpoints, so drop or split all of them before relying on endpoint-drop closure. Either endpoint or a combined handle can also callclose()explicitly.
Sender::try_send and Channel::try_send return TrySendError::Full(value) or
TrySendError::Closed(value) without waiting. Receiver::try_receive and
Channel::try_receive return Ok(value), Err(TryReceiveError::Empty) while
open and empty, or Err(TryReceiveError::Closed) once closed and drained.
These channel operations work with default-features = false, without Tokio:
use adamantium::{Channel, TryReceiveError};
let (sender, receiver) = Channel::bounded(2).split();
sender.try_send(42).unwrap();
drop(sender);
assert_eq!(receiver.try_receive(), Ok(42));
assert_eq!(receiver.try_receive(), Err(TryReceiveError::Closed));For explicit shutdown, use Tasks::run_until(cancellation, deadline, body) or
tasks.join_until(cancellation, deadline, body_result). Cancellation::new()
creates a cloneable signal: callers request shutdown with cancel(), async work
can await cancelled(), and blocking work can check is_cancelled(). The deadline
is an optional tokio::time::Instant; None needs no timer. Use a fresh signal
per scope. The scope also signals it on failure, deadline, completion, or drop.
These methods return ScopeError::Failed(error), Cancelled, or
DeadlineExceeded. A completed body error wins over an unclaimed child error,
which wins over shutdown; child panics are resumed after draining. If cancellation
and the deadline are both ready, cancellation wins. Once shutdown starts, its
reason stays fixed while cleanup runs. An already cancelled or expired scope
does not poll its body. As with run, child failures are collected when the body
asks for a value or joins; they do not interrupt an otherwise pending body.
Cancellation drops the body and aborts and drains async children before returning.
Synchronous destructors finish; asynchronous cleanup is not guaranteed to run.
Use tasks.spawn_blocking for blocking closures the scope must wait for, and let
them check the shared signal between bounded operations. Started blocking work
cannot be forcibly stopped, so cleanup can extend beyond the deadline. The free
blocking wrapper detaches its started closure if its future is dropped, including
when an enclosing async task is aborted. Dropping a scope future or panicking in
its body signals shutdown and requests aborts but cannot wait for cleanup. For
that guarantee, request cancellation and continue awaiting the scope.
Use concurrently_ordered(items, limit, f) to consume results in input order,
or concurrently_unordered(items, limit, f) for observed completion order
(simultaneous completions have unspecified order). Both return a Send stream
of Result<T, ScopeError<E>>. Pin it and consume it with StreamExt::next from
futures-util or another compatible stream library:
use adamantium::concurrently_ordered;
use futures_util::StreamExt;
#[tokio::main]
async fn main() -> Result<(), adamantium::ScopeError<std::io::Error>> {
let results = concurrently_ordered(0..100, 8, |n| async move {
Ok::<_, std::io::Error>(n * 2)
});
let mut results = std::pin::pin!(results);
while let Some(result) = results.next().await {
println!("{}", result?);
}
Ok(())
}cargo run --example streaming demonstrates both result orders, a slow
consumer, and explicit cancellation.
The window bounds started but not yet yielded items to max(limit, 1).
Running futures and buffered results share that bound. A slow first ordered
item therefore stops refill even when later items finish; the unordered variant
can yield those later results and refill their slots. This trades ordered
throughput for bounded storage. Memory is O(limit), excluding the caller's
iterator, individual values, and results the consumer retains. Neither variant
collects the input or spawns a background producer. Pausing consumption stops
new scheduling; already started tasks may finish into the bounded buffer.
Apart from into_iter, input consumption and closure calls happen only during
polling. Both must be quick and Send; input items need not be Send. Polling
the first nonempty window needs a Tokio runtime.
For shutdown, use concurrently_ordered_until(items, limit, cancellation, f)
or concurrently_unordered_until(...). These observe their supplied signal;
completion, error, panic, and drop never cancel it or sibling streams. This
differs from the fresh, scope-owned signal passed to Tasks::run_until.
Tasks can capture a clone to observe parent shutdown. A cancellation request
while consumption is paused takes effect when polling resumes.
On an observed error or cancellation, intake stops, outstanding async tasks
are aborted and drained, and buffered successes are discarded. The stream
yields one terminal error and then ends. A child panic is resumed after
draining and wins over the oldest recorded child error, which wins over
Cancelled. Errors bypass a slow earlier item in either order. Results already
yielded remain with the consumer. Unexpected runtime task cancellation drains
and panics if no child panic or error explains the missing result.
Dropping a pending next() while retaining the stream preserves its work.
Dropping the stream, or a panic in the iterator/closure itself, requests aborts
without waiting. To await cleanup, request cancellation and keep polling to the
terminal error. Only async tasks are registered: asynchronous cleanup is not
guaranteed, and detached work such as a started blocking closure can outlive
the stream.
Streaming uses optional futures-core for the standard Stream trait and
async-stream for the yielding state machine, both enabled by the existing
tokio feature. futures-util is a dev dependency for examples and tests;
consumers using its extensions add it themselves. Disabling default features
excludes Tokio and async-stream; futures-core is still used transitively by
the existing runtime-independent channel dependency.
With serde, Bytes accepts numeric sequences in human-readable formats and
byte strings in binary formats; JSON strings are rejected. Binary byte strings
currently fail to deserialize when Serde buffers them for #[serde(flatten)],
#[serde(untagged)], internally tagged enums (#[serde(tag = "...")]), or
adjacently tagged enums (#[serde(tag = "...", content = "...")]) when the
content comes before the tag. Serde's buffer reports itself as human-readable,
so Bytes requests a sequence instead of a byte string. This hint preserves
support for text formats such as quick-xml, csv, and envy, whose
deserialize_any does not produce a sequence.
The lint levels are in this repository's [workspace.lints], and the Clippy configuration is in clippy.toml. cargo adamantium rules prints both. The rules and library provide the following properties for code that follows them:
| Property | Enforced by |
|---|---|
No unsafe code |
unsafe_code = "forbid" |
| Every value can cross a task | future_not_send; Rc, RefCell, Cell, and LocalSet disallowed, and so are LocalRuntime's entry points |
| No task outlives its scope | tokio::spawn, spawn_local, JoinSet, the spawn methods of Tokio's Handle, Runtime, LocalRuntime, and task::Builder, std::thread::spawn, and scope disallowed; tasks use Tasks |
| No lock is held across an await or inside another lock | await_holding_lock; Mutex and RwLock from std, tokio, parking_lot, and futures disallowed; Actor::with is synchronous and rejects nesting |
| No worker thread is blocked | std::thread::sleep, block_in_place, every Tokio spawn_blocking, and block_on on a Handle or LocalRuntime disallowed; blocking work uses blocking |
No panic from indexing, unwrapping, or an explicit panic! |
indexing_slicing, string_slice, unwrap_used, expect_used, panic, todo, unimplemented, unreachable |
| No silent truncation or wrap | cast_possible_truncation, cast_possible_wrap, cast_sign_loss; overflow-checks = true in release |
| No leaks or cycles | mem_forget, Box::leak, Arc::downgrade, and Weak disallowed |
| No skipped cleanup | exit |
Tests are exempt from the panic rules, so unwrap, indexing, and panic! are allowed in #[test] functions and #[cfg(test)] modules. Clippy does not treat helper functions in integration test files as tests, so those functions follow the rules.
The rules have limits. A receive can wait indefinitely if no message arrives and the channel remains open. Dependencies can still use unsafe code or panic, and Clippy cannot detect every call that blocks a worker thread. Runtime::block_on is allowed because #[tokio::main] and #[tokio::test] expand to it; called inside a runtime, it panics at run time instead of blocking. Arithmetic overflow and stack exhaustion can still panic.
cargo adamantium style check uses the OpenAI Decisions API to review the judgment-based parts of the Google Rust style guide. It returns findings for review; it does not edit source or replace rustfmt, Clippy, or code review.
cargo adamantium style check # every .rs file under the current directory
cargo adamantium style check src/lib.rs --context "application code"
cargo adamantium style check . --rule unnecessary_deep_clone --dry-run
cargo adamantium style rules --format textThe command prints JSON by default, or a readable report with --format text. It finds Rust files recursively, respects ignore files, and skips hidden directories, target, and node_modules. It does not resolve Cargo workspace membership. Each file is sent to the API in a request with the selected rules and any context. Findings apply to the whole file because Decisions does not provide reliable locations. The file, path, and context must fit within the API's 256 KiB JSON input limit. The exit status is 0 for a clean check or dry run, 1 for findings, and 2 for an incomplete check.
Transient failures are retried with exponential backoff and jitter, up to four attempts within three minutes per file. Retry-After is honored up to 30 seconds. Authentication, quota, and spending-limit errors are not retried. A service failure stops further requests and marks the remaining files as unchecked. Use --retry-from previous.json to retry failed and unchecked files in a new report. Supply the same scan root and --rule options, and pass --context again if needed. Do not redirect the output over the report being read.
Set OPENAI_API_KEY. On macOS, the openai-api-key Keychain item is used when the variable is unset. Source files and context are sent to OpenAI. Results are model estimates. Evaluate them on labeled examples before using them as a blocking check. The questions instruct the model to treat source comments and strings as untrusted evidence, but this does not guarantee resistance to adversarial input.
The rules are in style/rules/google-rust-decisions.json, with their guide coverage in style/rules/coverage.md and labeled examples in style/fixtures/google-rust-cases.json. Run node style/scripts/evaluate-decisions.mjs to evaluate the examples against the API. It also accepts --dry-run and --rule. The full run makes 114 billable requests.
cargo fmt --all -- --check
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
cargo test --locked --workspace --all-features
cargo build --locked -p adamantium --no-default-features
cargo clippy --locked -p adamantium --all-targets --no-default-features -- -D warnings
cargo test --locked -p adamantium --no-default-features
node --test style/scripts/evaluate-decisions.test.mjsGitHub Actions runs the same commands on pull requests and pushes to main.
The repository follows its own rules. Where the library needs restricted features, such as the JoinSet inside Tasks and the Mutex inside Actor, the code uses an allow with a reason. The CLI tests run cargo adamantium check against fixture crates with and without rule violations. They require Clippy and network access on the first run.