Skip to content

Commit 56d8ba4

Browse files
committed
safety critical
1 parent 683dce3 commit 56d8ba4

File tree

14 files changed

+331
-3
lines changed

14 files changed

+331
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["crates/algorithms/*", "crates/concurrency/*", "crates/development_tools/debugging/tracing", "crates/web", "xtask"]
2+
members = ["crates/algorithms/*", "crates/concurrency/*", "crates/development_tools/debugging/tracing", "crates/safety_critical/*", "crates/web", "xtask"]
33

44
[workspace.package]
55
edition = "2024"

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ dev: build test ## Build and test (development workflow)
2121
@echo "Development build complete!"
2222

2323
deploy: clean dev ## Deploy to GitHub Pages (requires maintainer permissions)
24-
./scripts/deploy.sh
24+
./scripts/deploy.sh --skip-tests
2525

2626
deploy-skip-tests: build ## Deploy to GitHub Pages without running tests
2727
./scripts/deploy.sh --skip-tests
@@ -32,4 +32,4 @@ clean: ## Clean build artifacts
3232
@echo "Clean complete!"
3333

3434
serve: install-mdbook ## Serve the book locally with live reload
35-
mdbook serve --open
35+
mdbook serve --open

build.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ const REMOVED_PREFIXES: &[&str] = &[
1212
"./src/development_tools/debugging/tracing/",
1313
"./src/concurrency/actor/",
1414
"./src/concurrency/custom_future/",
15+
"./src/safety_critical/no_panic/",
16+
"./src/safety_critical/heapless_alloc/",
1517
];
1618

1719
fn main() {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "heapless-example"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
publish.workspace = true
8+
9+
[dependencies]
10+
heapless = "0.8"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use heapless::{String, Vec};
2+
3+
/// A fixed-capacity event log that never touches the heap.
4+
///
5+
/// In real-time and safety-critical systems the global allocator is
6+
/// forbidden because allocation time is unbounded and fragmentation
7+
/// can cause silent OOM failures in long-running firmware.
8+
///
9+
/// [`heapless::Vec`] stores up to `N` elements on the stack (or in a
10+
/// `static`). The capacity is fixed at compile time, so the memory
11+
/// footprint is constant and predictable.
12+
struct EventLog<const N: usize> {
13+
entries: Vec<Event, N>,
14+
}
15+
16+
#[derive(Debug, Clone)]
17+
struct Event {
18+
timestamp_ms: u32,
19+
code: u16,
20+
}
21+
22+
impl<const N: usize> EventLog<N> {
23+
/// Creates an empty log.
24+
fn new() -> Self {
25+
Self {
26+
entries: Vec::new(),
27+
}
28+
}
29+
30+
/// Records an event. Returns `Err` if the log is full instead
31+
/// of panicking or allocating—the caller decides what to do.
32+
fn record(&mut self, timestamp_ms: u32, code: u16) -> Result<(), Event> {
33+
let event = Event { timestamp_ms, code };
34+
self.entries.push(event).map_err(|e| e)
35+
}
36+
37+
/// Returns how many events have been recorded.
38+
fn len(&self) -> usize {
39+
self.entries.len()
40+
}
41+
42+
/// Returns the most recent event, if any.
43+
fn latest(&self) -> Option<&Event> {
44+
self.entries.last()
45+
}
46+
}
47+
48+
/// Formats a sensor label without heap allocation.
49+
///
50+
/// [`heapless::String<N>`] works like `std::string::String` but
51+
/// stores up to `N` bytes on the stack. `write!` returns `Err` if
52+
/// the formatted text would exceed capacity.
53+
fn format_label(sensor_id: u16, value: f32) -> Result<String<32>, core::fmt::Error> {
54+
use core::fmt::Write;
55+
let mut buf: String<32> = String::new();
56+
write!(buf, "S{sensor_id}={value:.1}")?;
57+
Ok(buf)
58+
}
59+
60+
fn main() {
61+
// A log that holds at most 8 events — zero heap allocation.
62+
let mut log: EventLog<8> = EventLog::new();
63+
64+
log.record(100, 0x01).expect("log not full");
65+
log.record(200, 0x02).expect("log not full");
66+
log.record(300, 0xFF).expect("log not full");
67+
68+
println!("logged {} events", log.len());
69+
println!("latest: {:?}", log.latest().unwrap());
70+
71+
// Stack-allocated string formatting.
72+
let label = format_label(42, 3.14).expect("fits in 32 bytes");
73+
println!("label: {label}");
74+
75+
// Demonstrate capacity enforcement — the 9th push returns Err.
76+
let mut full_log: EventLog<2> = EventLog::new();
77+
full_log.record(0, 1).unwrap();
78+
full_log.record(1, 2).unwrap();
79+
let overflow = full_log.record(2, 3);
80+
assert!(overflow.is_err());
81+
println!("overflow correctly rejected");
82+
}
83+
84+
#[cfg(test)]
85+
mod tests {
86+
use super::*;
87+
88+
#[test]
89+
fn test_event_log_records_and_retrieves() {
90+
let mut log: EventLog<4> = EventLog::new();
91+
log.record(10, 0xAA).unwrap();
92+
log.record(20, 0xBB).unwrap();
93+
94+
assert_eq!(log.len(), 2);
95+
assert_eq!(log.latest().unwrap().code, 0xBB);
96+
}
97+
98+
#[test]
99+
fn test_event_log_rejects_overflow() {
100+
let mut log: EventLog<1> = EventLog::new();
101+
assert!(log.record(0, 1).is_ok());
102+
assert!(log.record(1, 2).is_err());
103+
assert_eq!(log.len(), 1);
104+
}
105+
106+
#[test]
107+
fn test_format_label() {
108+
let label = format_label(7, 25.0).unwrap();
109+
assert_eq!(label.as_str(), "S7=25.0");
110+
}
111+
112+
#[test]
113+
fn test_format_label_overflow() {
114+
// heapless::String<4> can only hold 4 bytes — "S1=0.0" won't fit.
115+
use core::fmt::Write;
116+
let mut tiny: heapless::String<4> = heapless::String::new();
117+
let result = write!(tiny, "S1=99.9");
118+
assert!(result.is_err());
119+
}
120+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "no-panic-example"
3+
version.workspace = true
4+
authors.workspace = true
5+
edition.workspace = true
6+
license.workspace = true
7+
publish.workspace = true
8+
9+
[dependencies]
10+
no-panic = "0.1"
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use no_panic::no_panic;
2+
3+
/// Sums a slice without any operation that could panic.
4+
///
5+
/// In safety-critical code, a `panic!` is a catastrophic failure.
6+
/// Standard indexing like `slice[i]` inserts a bounds check that
7+
/// calls `panic!` on out-of-bounds access. Iterators avoid this
8+
/// entirely—the compiler can prove no panic path exists.
9+
#[no_panic]
10+
fn safe_sum(values: &[i32]) -> i64 {
11+
let mut total: i64 = 0;
12+
for &v in values {
13+
total += v as i64;
14+
}
15+
total
16+
}
17+
18+
/// Looks up a value by index, returning `None` instead of panicking.
19+
///
20+
/// Using `get()` + exhaustive pattern matching guarantees every code
21+
/// path is handled. The `#[no_panic]` attribute makes the compiler
22+
/// **prove** it at link time—if any hidden panic path remains, the
23+
/// build fails.
24+
#[no_panic]
25+
fn safe_lookup(data: &[u8], index: usize) -> Option<u8> {
26+
match data.get(index) {
27+
Some(&val) => Some(val),
28+
None => None,
29+
}
30+
}
31+
32+
/// Clamps a sensor reading into a valid range without panicking.
33+
///
34+
/// Real-world example: an ADC returns a raw `u16` that must be
35+
/// mapped to 0–100 %. Using `clamp` and simple arithmetic keeps
36+
/// the function panic-free.
37+
#[no_panic]
38+
fn normalize_sensor(raw: u16, min: u16, max: u16) -> f32 {
39+
if max == min {
40+
return 0.0;
41+
}
42+
let clamped = raw.clamp(min, max);
43+
(clamped - min) as f32 / (max - min) as f32
44+
}
45+
46+
fn main() {
47+
let readings = [10, 20, 30, 40, 50];
48+
49+
let total = safe_sum(&readings);
50+
println!("sum = {total}");
51+
assert_eq!(total, 150);
52+
53+
let val = safe_lookup(&[0xAA, 0xBB, 0xCC], 1);
54+
println!("lookup index 1 = {val:?}");
55+
assert_eq!(val, Some(0xBB));
56+
57+
let miss = safe_lookup(&[0xAA, 0xBB, 0xCC], 99);
58+
println!("lookup index 99 = {miss:?}");
59+
assert_eq!(miss, None);
60+
61+
let pct = normalize_sensor(2048, 0, 4095);
62+
println!("sensor = {pct:.2}%");
63+
assert!((pct - 0.5).abs() < 0.01);
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::*;
69+
70+
#[test]
71+
fn test_safe_sum_empty() {
72+
assert_eq!(safe_sum(&[]), 0);
73+
}
74+
75+
#[test]
76+
fn test_safe_sum_values() {
77+
assert_eq!(safe_sum(&[1, 2, 3]), 6);
78+
}
79+
80+
#[test]
81+
fn test_safe_lookup_in_bounds() {
82+
assert_eq!(safe_lookup(&[10, 20, 30], 2), Some(30));
83+
}
84+
85+
#[test]
86+
fn test_safe_lookup_out_of_bounds() {
87+
assert_eq!(safe_lookup(&[10, 20, 30], 5), None);
88+
}
89+
90+
#[test]
91+
fn test_normalize_sensor_mid() {
92+
let pct = normalize_sensor(2048, 0, 4095);
93+
assert!((pct - 0.5).abs() < 0.01);
94+
}
95+
96+
#[test]
97+
fn test_normalize_sensor_equal_bounds() {
98+
assert_eq!(normalize_sensor(100, 100, 100), 0.0);
99+
}
100+
}

src/SUMMARY.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@
5858
- [Complex Numbers](science/mathematics/complex_numbers.md)
5959
- [Statistics](science/mathematics/statistics.md)
6060
- [Miscellaneous](science/mathematics/miscellaneous.md)
61+
- [Safety-Critical Rust](safety_critical.md)
62+
- [No-Panic Guarantee](safety_critical/no_panic.md)
63+
- [Deterministic Memory](safety_critical/heapless_alloc.md)
6164
- [Text Processing](text.md)
6265
- [Regular Expressions](text/regex.md)
6366
- [String Parsing](text/string_parsing.md)

src/links.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Keep lines sorted.
1919
[cat-cryptography]: https://crates.io/categories/cryptography
2020
[cat-database-badge]: https://img.shields.io/badge/database--x.svg?style=social
2121
[cat-database]: https://crates.io/categories/database
22+
[cat-data-structures-badge]: https://img.shields.io/badge/data_structures--x.svg?style=social
23+
[cat-data-structures]: https://crates.io/categories/data-structures
2224
[cat-date-and-time-badge]: https://img.shields.io/badge/date_and_time--x.svg?style=social
2325
[cat-date-and-time]: https://crates.io/categories/date-and-time
2426
[cat-debugging-badge]: https://img.shields.io/badge/debugging--x.svg?style=social
@@ -80,6 +82,8 @@ Keep lines sorted.
8082
[flate2]: https://docs.rs/flate2/
8183
[glob-badge]:https://img.shields.io/crates/v/glob.svg?label=glob
8284
[glob]: https://docs.rs/glob/
85+
[heapless-badge]: https://img.shields.io/crates/v/heapless.svg?label=heapless
86+
[heapless]: https://docs.rs/heapless/
8387
[hyper-badge]: https://img.shields.io/crates/v/hyper.svg?label=hyper
8488
[hyper]: https://docs.rs/hyper/
8589
[image-badge]: https://img.shields.io/crates/v/image.svg?label=image
@@ -98,6 +102,8 @@ Keep lines sorted.
98102
[nalgebra]: https://docs.rs/nalgebra
99103
[ndarray-badge]: https://img.shields.io/crate/ndarray.svg?label=ndarray
100104
[ndarray]: https://docs.rs/ndarray
105+
[no-panic-badge]: https://img.shields.io/crates/v/no-panic.svg?label=no-panic
106+
[no-panic]: https://docs.rs/no-panic/
101107
[num-badge]: https://img.shields.io/crates/v/num.svg?label=num
102108
[num]: https://docs.rs/num/
103109
[num_cpus-badge]: https://img.shields.io/crates/v/num_cpus.svg?label=num_cpus

src/safety_critical.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Safety-Critical Rust
2+
3+
| Recipe | Crates | Categories |
4+
|--------|--------|------------|
5+
| [Compile-Time No-Panic Guarantee][ex-no-panic] | [![no-panic-badge]][no-panic] | [![cat-no-std-badge]][cat-no-std] [![cat-rust-patterns-badge]][cat-rust-patterns] |
6+
| [Deterministic Memory with Heapless Collections][ex-heapless] | [![heapless-badge]][heapless] | [![cat-no-std-badge]][cat-no-std] [![cat-data-structures-badge]][cat-data-structures] |
7+
8+
[ex-no-panic]: safety_critical/no_panic.html#compile-time-no-panic-guarantee
9+
[ex-heapless]: safety_critical/heapless_alloc.html#deterministic-memory-with-heapless-collections
10+
11+
{{#include links.md}}

0 commit comments

Comments
 (0)