Skip to content

Repository files navigation

Redline

A syntactic lint for allocation and syscall constructs in hot functions.

crates.ioLicense

Quick Start | Constraints | Cost Model | Limitations


What Redline actually is

Redline is a proc-macro that walks the annotated function's own syntax tree and emits a hard compile_error! when it finds a construct from a fixed list of recognized allocation and syscall constructs, or when a weighted node count exceeds a number you wrote.

That is the whole mechanism. It is a lint with a hard failure mode.

What Redline is not

It does not prove anything about performance, and it cannot.

  • It has no type information, no name resolution, no MIR, no LLVM IR, no compiled code. It sees tokens.
  • It cannot see into any function you call. helper() could allocate a gigabyte; redline has no way to know. (It now refuses to guess — see Un-analyzable calls.)
  • Its latency numbers are uncalibrated weights on pre-optimization source nodes. rustc will inline, vectorize, hoist and delete much of what is counted. A passing latency = "< 1ms" is not evidence that the function runs in under 1ms, and a failing one is not evidence that it doesn't.
  • Real worst-case execution time analysis operates on IR or on the compiled binary with a hardware timing model, precisely because source-level estimation does not work. See Prior art.

Use it to keep String::from and println! out of a hot loop. Do not use it as a performance guarantee.

Install

[dependencies]
alia-redline = "0.3"

Usage

use redline::redline;// Rejects the recognized allocation constructs: Vec/String/Box/HashMap// constructors, .to_string(), .to_owned(), .collect(), vec!, format!, ...#[redline(allocs = 0)]fnhot_path(a:f64,b:f64) -> f64{
a * a + b * b
}// Rejects the recognized syscall constructs: std::fs::*, File::open,// TcpStream::connect, println!, ...#[redline(syscalls = 0)]fnpure_compute(x:i32) -> i32{
x * x + 2* x + 1}// Compares a weighted AST node count against a number. Uncalibrated.#[redline(latency = "< 1ms", allocs = 0, syscalls = 0)]fncritical_section(data:&[u8]) -> u64{letmut h:u64 = 0;for i in0..64{
h ^= data.get(i).copied().unwrap_or(0)asu64;}
h
}

What a violation looks like

#[redline(allocs = 0)]fnoops() -> String{String::from("hello")}
error: redline: function `oops` contains 1 recognized allocation(s), limit is 0
--> src/main.rs:2:4

Un-analyzable calls

This used to compile clean, and that was the single worst bug in the crate:

#[redline(allocs = 0)]fnbody_local_only() -> usize{helper().len()// helper allocates. Redline never looked.}

A proc-macro cannot follow a call — there is no cross-crate MIR, no type information, not even reliable name resolution. So redline no longer pretends. Any call it does not recognize is a hard error:

error: redline: cannot analyze `helper`: this path is not in redline's
recognized-construct table. Redline only sees this function's own syntax tree,
so this call's allocations, syscalls and latency are unknown and the constraint
cannot be checked. Inline the work, use a construct redline recognizes, or add
`assume_calls_free` to the attribute to state that un-analyzable calls are
irrelevant here (nothing verifies that).

The escape hatch is explicit and unverified by construction:

#[redline(allocs = 0, assume_calls_free)]fndelegates(x:u64) -> u64{helper(x)*2// unchecked. You are asserting, not proving.}

.await is treated the same way: suspension time is not a syntactic property.

This means redline is only useful on leaf-ish functions written in terms of primitives, slices, iterators and the constructs in its tables. That is a real restriction, and it is the honest size of what a proc-macro can check.

Supported constraints

ConstraintSyntaxWhat is actually checked
Allocationsallocs = 0, allocs = "< 5"Count of constructs in redline's allocation table
Syscallssyscalls = 0, syscalls = "< 5"Count of constructs in redline's syscall table
Latencylatency = "< 1ms"Uncalibrated weighted node count vs. the bound
Stack sizemax_stack = "< 4KB"number of bindings × 8 bytes. There is no type information, so this is close to meaningless
Cost modelcost_model = "ns" / "cycles"Which weight table to use
Escape hatchassume_calls_freeSuppresses the un-analyzable-call error

Time units: ns, us, ms, s, cycles (with cost_model = "cycles"). Size units: B, KB, MB, GB.

Removed: throughput

throughput = "> 1GB/s" was documented as enforced. It parsed and then checked nothing — the match arm was empty. Throughput is bytes per unit of wall-clock time; an AST pass sees neither operand. It is now a hard error, because silently accepting an annotation that checks nothing is worse than rejecting it.

How the matcher works

Recognition is done on syn::Pathsegments, comparing whole identifiers, requiring a suffix match of at least two segments:

  • std::fs::read, fs::read → syscall.
  • read, spread, myfs::read, fs::readable → not recognized.

The previous implementation ran .contains() on the stringified path against substrings like "read", "bind", "lock" and "accept". A user function named spread, rebind, unlock or blocked was reported as a syscall. There are now regression tests for exactly those names (src/cost.rs unit tests, tests/pass/no_false_syscalls.rs).

Method calls have no receiver type available, so they are matched on name alone: a small allocating list (to_string, collect, …), a small free list (len, get, wrapping_*, iterator adapters), and everything else is un-analyzable. A user type with a method named collect that allocates nothing is over-counted. That direction is deliberate: over-counting rejects, under-counting lies.

Cost model

CostTable::NS in src/cost.rs:

ConstructWeight
ALU op, branch, index1
Call5
Heap allocation50
format!200
Syscall1,000
println!5,000
Network syscall10,000

Loops with a literal range use the exact trip count. Every other loop — while, loop, for x in xs — is assumed to run 1,000 iterations. That number is arbitrary. A loop that runs a million times is under-counted by 1000x.

These weights were never calibrated. benchmarks/bench_cost_model.py measures each one against a microbenchmark on your machine and reports the ratio; on the development machine the println! weight is ~26,000x the measured cost of a buffered write and the file-syscall weight is ~14x too small. That benchmark compares isolated release-build microbenchmarks against pre-optimization source counts, so it does not validate whole-function estimates either.

There is no accuracy number for this crate. The previous one (100% accuracy, 0 false positives) was computed over 14 snippets hand-written to contain exactly the constructs the matcher looks for. That benchmark and its results file have been deleted rather than restated. Building a credible corpus means hand- labelling allocation and syscall behaviour of real third-party crate functions; until that exists, no number is claimed.

Prior art

The claim "nothing does this" was false. Redline is the weakest tool in this list; it is a lint, and everything below does more than it does.

ToolWhat it doesRelation to Redline
iai-callgrindDeterministic instruction/cache counts on the compiled binary via Callgrind, with regression thresholds in CIWhat you should use if you want reproducible cost numbers. Operates on real machine code.
#[no_panic]Turns "this function may panic" into a link errorDirect precedent for property-violation-as-build-failure, and it works because the linker sees the optimized program. Redline's check is strictly weaker.
KaniBounded model checking of Rust via CBMCReal compile-time verification. Proofs, not heuristics.
PrustiDeductive verification (Viper) with pre/postconditionsReal verification of functional properties.
MIRAIAbstract interpretation over MIRAnalyses MIR, follows calls — the thing redline cannot do.
CreusotDeductive verification via Why3Real proofs over MIR.
static-assertionsconst-evaluable assertions (sizes, alignment, trait impls) as compile errorsSame "compile error on violated property" shape, on properties that are actually decidable at compile time.
cargo-bloatPer-function size in the built binaryMeasures the artifact instead of guessing from source.
ClippyLints, including perf lintsNearest neighbour. Redline's contribution over clippy is the per-function budget syntax and the hard error.
WCET analysers (aiT, OTAWA, Heptane)Worst-case execution time for real-time/avionicsOperate on binaries or IR with a hardware timing model and require loop-bound annotations. They work at that level because source-AST estimation is known not to work.

Architecture

src/
lib.rs Proc-macro entry point (#[redline(...)])
parse.rs Attribute parser; rejects anything unenforceable
analyze.rs AST walker: counts recognized constructs, flags un-analyzable calls
cost.rs Weight tables + the path/method recognition tables

Tests: cargo test runs 18 unit tests (path matching, false-positive regressions, opt-out behaviour) plus trybuild compile-pass/compile-fail cases in tests/.

Known unsoundness

Not a roadmap — a list of ways the tool is wrong today.

  • Macro bodies are not expanded. An unrecognized macro is counted as one call and its contents are invisible.
  • Method recognition is by name, with no receiver type. Both false positives and false negatives are possible.
  • Trait dispatch, generics and operator overloading are invisible. a + b is counted as one ALU op even if Add is implemented by allocating.
  • Drop is invisible. Dropping a Vec deallocates; nothing counts that.
  • Non-literal loop bounds are guessed at 1,000 iterations.
  • max_stack counts bindings, not sizes.
  • assume_calls_free disables the main safety net entirely.

License

Apache-2.0 | ALIA Labs

Built by Tushar Sharma at ALIA Labs.

About

Cross the performance line. Won't compile. Compile-time performance guarantees for Rust.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages