A syntactic lint for allocation and syscall constructs in hot functions.
Quick Start | Constraints | Cost Model | Limitations
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.
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.
[dependencies]
alia-redline = "0.3"use redline::redline;
// Rejects the recognized allocation constructs: Vec/String/Box/HashMap
// constructors, .to_string(), .to_owned(), .collect(), vec!, format!, ...
#[redline(allocs = 0)]
fn hot_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)]
fn pure_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)]
fn critical_section(data: &[u8]) -> u64 {
let mut h: u64 = 0;
for i in 0..64 {
h ^= data.get(i).copied().unwrap_or(0) as u64;
}
h
}#[redline(allocs = 0)]
fn oops() -> String {
String::from("hello")
}error: redline: function `oops` contains 1 recognized allocation(s), limit is 0
--> src/main.rs:2:4
This used to compile clean, and that was the single worst bug in the crate:
#[redline(allocs = 0)]
fn body_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)]
fn delegates(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.
| Constraint | Syntax | What is actually checked |
|---|---|---|
| Allocations | allocs = 0, allocs = "< 5" |
Count of constructs in redline's allocation table |
| Syscalls | syscalls = 0, syscalls = "< 5" |
Count of constructs in redline's syscall table |
| Latency | latency = "< 1ms" |
Uncalibrated weighted node count vs. the bound |
| Stack size | max_stack = "< 4KB" |
number of bindings × 8 bytes. There is no type information, so this is close to meaningless |
| Cost model | cost_model = "ns" / "cycles" |
Which weight table to use |
| Escape hatch | assume_calls_free |
Suppresses the un-analyzable-call error |
Time units: ns, us, ms, s, cycles (with cost_model = "cycles").
Size units: B, KB, MB, GB.
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.
Recognition is done on syn::Path segments, 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.
CostTable::NS in src/cost.rs:
| Construct | Weight |
|---|---|
| ALU op, branch, index | 1 |
| Call | 5 |
| Heap allocation | 50 |
format! |
200 |
| Syscall | 1,000 |
println! |
5,000 |
| Network syscall | 10,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.
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.
| Tool | What it does | Relation to Redline |
|---|---|---|
| iai-callgrind | Deterministic instruction/cache counts on the compiled binary via Callgrind, with regression thresholds in CI | What you should use if you want reproducible cost numbers. Operates on real machine code. |
#[no_panic] |
Turns "this function may panic" into a link error | Direct precedent for property-violation-as-build-failure, and it works because the linker sees the optimized program. Redline's check is strictly weaker. |
| Kani | Bounded model checking of Rust via CBMC | Real compile-time verification. Proofs, not heuristics. |
| Prusti | Deductive verification (Viper) with pre/postconditions | Real verification of functional properties. |
| MIRAI | Abstract interpretation over MIR | Analyses MIR, follows calls — the thing redline cannot do. |
| Creusot | Deductive verification via Why3 | Real proofs over MIR. |
| static-assertions | const-evaluable assertions (sizes, alignment, trait impls) as compile errors |
Same "compile error on violated property" shape, on properties that are actually decidable at compile time. |
| cargo-bloat | Per-function size in the built binary | Measures the artifact instead of guessing from source. |
| Clippy | Lints, including perf lints |
Nearest 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/avionics | Operate 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. |
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/.
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 + bis counted as one ALU op even ifAddis implemented by allocating. Dropis invisible. Dropping aVecdeallocates; nothing counts that.- Non-literal loop bounds are guessed at 1,000 iterations.
max_stackcounts bindings, not sizes.assume_calls_freedisables the main safety net entirely.
Apache-2.0 | ALIA Labs
Built by Tushar Sharma at ALIA Labs.