Project 5: Const Generic Matrix - Type-Level Linear Algebra
Build a Matrix library where dimensions (rows/columns) are encoded in the type system, enabling compile-time dimension checking for all matrix operations. This eliminates an entire class of runtime errors by making invalid matrix operations impossible to express.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 2: Intermediate |
| Time Estimate | 1-2 weeks |
| Language | Rust |
| Prerequisites | Basic Rust (ownership, traits, generics), elementary linear algebra |
| Key Topics | Const generics, monomorphization, operator overloading, zero-cost abstractions |
1. Learning Objectives
By completing this project, you will:
- Understand const generics - How to use constant values as type parameters (
const N: usize) - Master monomorphization - How the compiler generates specialized code for each matrix size
- Implement operator traits - Overload
+,-,*for custom types with proper trait bounds - Apply type-level constraints - Enforce mathematical rules (M x N * N x P = M x P) at compile time
- Design zero-cost abstractions - Prove through benchmarks that type safety has no runtime overhead
- Use fixed-size arrays - Understand why
[[T; C]; R]beatsVec<Vec<T>>for known dimensions - Explore nightly features - Learn about
generic_const_exprsfor computed const values - Build production-quality APIs - Create ergonomic interfaces with
new,zeros,identity,transpose - Write comprehensive tests - Including compile-fail tests that verify type errors
- Connect to real libraries - Understand how nalgebra, ndarray, and graphics engines use these patterns
2. Theoretical Foundation
2.1 Core Concepts
Type-Level Programming: Encoding Values in Types
In most programming, values exist at runtime:
// Runtime value - known when program runs
let rows: usize = 3;
let cols: usize = 4;
In type-level programming, values exist at compile time:
// Compile-time value - known when program is compiled
struct Matrix<const R: usize, const C: usize>;
let m: Matrix<3, 4>; // The "3" and "4" ARE the type
This shift is profound. When dimensions are part of the type:
Regular Matrix: Const Generic Matrix:
+------------------+ +------------------+
| rows: usize | | NO RUNTIME DATA |
| cols: usize | | Type IS the data |
| data: Vec<f32> | | data: [[f32;C];R]|
+------------------+ +------------------+
| |
v v
Runtime check: Compile-time check:
if rows != other.cols The code won't compile
panic!("Mismatch!") if dimensions mismatch
Monomorphization: How the Compiler Specializes Your Code
When you write generic code, Rust doesn’t create one “generic” function. It creates a specialized copy for each concrete type used:
// You write this:
impl<const R: usize, const C: usize> Matrix<R, C> {
pub fn zeros() -> Self {
Self { data: [[0.0; C]; R] }
}
}
// The compiler generates these (conceptually):
impl Matrix<2, 2> {
pub fn zeros() -> Self {
Self { data: [[0.0, 0.0], [0.0, 0.0]] }
}
}
impl Matrix<3, 4> {
pub fn zeros() -> Self {
Self { data: [[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0]] }
}
}
Source Code Compiled Binary
+------------------+ +------------------+
| Matrix<R, C> | | Matrix_2_2 |
| generic code | ==========> | Matrix_3_4 |
| | rustc | Matrix_10_10 |
+------------------+ +------------------+
(specialized copies)
Tradeoff: More sizes = larger binary. But each function is optimized perfectly for its specific size.
2.2 Why This Matters
Compile-time dimension checking eliminates entire categories of bugs:
RUNTIME CHECKING (Traditional)
fn multiply(a: DynMatrix, b: DynMatrix) -> Result<DynMatrix, Error> {
if a.cols != b.rows { // Check happens at RUNTIME
return Err("Dimension mismatch!");
}
// ... perform multiplication
}
COMPILE-TIME CHECKING (Const Generics)
fn multiply<const R1: usize, const C1: usize, const C2: usize>
(a: Matrix<R1, C1>, b: Matrix<C1, C2>) -> Matrix<R1, C2>
{ ^^^ ^^^
// No check needed! These MUST match or code won't compile
}
Real-world impact:
- NASA Mars Climate Orbiter: Lost due to unit conversion error (cost: $327 million)
- Graphics programming: Shader matrix mismatches cause visual artifacts or crashes
- Machine learning: Tensor shape errors are the #1 debugging pain point
2.3 Historical Context
Const generics were one of Rust’s most requested features:
2017: RFC 2000 proposed (Const Generics)
"Allow generic parameters over constant values"
2019: Partial implementation lands (min_const_generics)
Limited to integers, no const expressions
2021: Rust 1.51 stabilizes min_const_generics
- Arrays can use const generics: [T; N]
- Structs can have const parameters
- Basic arithmetic NOT allowed yet
2023+: generic_const_exprs (nightly)
- Allows R * C computations
- Enables more complex type math
Before const generics, you had two bad options:
// Option 1: Macros (ugly, slow to compile)
macro_rules! matrix {
($rows:expr, $cols:expr) => {
struct Matrix { data: [[f32; $cols]; $rows] }
};
}
// Option 2: Runtime dimensions (slow, error-prone)
struct Matrix {
rows: usize,
cols: usize,
data: Vec<f32>,
}
2.4 Common Misconceptions
Misconception 1: “Const generics add runtime overhead”
- Reality: They add ZERO runtime overhead. The type parameters exist only at compile time.
Misconception 2: “I can do arithmetic with const generics on stable Rust”
- Reality: Complex const expressions like
R * Crequire the nightlygeneric_const_exprsfeature.
Misconception 3: “Type State pattern is the same as const generics”
- Reality: Type State uses phantom type parameters; const generics use actual values in types.
Misconception 4: “More matrix sizes = slower code”
- Reality: Each specialized version is perfectly optimized. The code is FASTER, but binary is larger.
3. Project Specification
3.1 What You Will Build
A Matrix library where the dimensions (rows/cols) are part of the type. Multiplying a Matrix<3, 2> by a Matrix<2, 4> is allowed, but multiplying by a Matrix<5, 5> will fail to compile.
Example Code:
let a = Matrix::<3, 2>::new([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]);
let b = Matrix::<2, 4>::new([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]]);
let c = a * b; // Compiles! Result is Matrix<3, 4>
let d = Matrix::<5, 5>::new();
let e = a * d; // Error: "expected Matrix<2, _>, found Matrix<5, 5>"
3.2 Functional Requirements
- Define
Matrix<const R: usize, const C: usize>struct - Implement
new(),zeros(),ones()constructors - Implement
Debugtrait for pretty-printing - Implement
Addtrait for same-dimension matrices - Implement
Subtrait for same-dimension matrices - Implement
Multrait with dimension checking (cols of A = rows of B) - Implement scalar multiplication
- Implement
transpose()returningMatrix<C, R> - Implement
identity()for square matrices only - Implement
IndexandIndexMutfor element access - Write compile-fail tests proving invalid operations don’t compile
3.3 Non-Functional Requirements
- Zero runtime overhead: Prove via benchmarks that performance matches hand-written arrays
- No heap allocation: Use fixed-size arrays, not Vec
- Ergonomic API: Support natural syntax like
a + banda * b - Comprehensive documentation: Every public item documented
3.4 Example Usage / Output
$ cargo new --lib const-matrix
Created library `const-matrix` package
$ cd const-matrix
$ cargo test
Compiling const-matrix v0.1.0
Finished test [unoptimized + debuginfo] target(s) in 1.23s
Running unittests src/lib.rs
running 8 tests
test tests::test_zeros ... ok
test tests::test_ones ... ok
test tests::test_addition ... ok
test tests::test_subtraction ... ok
test tests::test_scalar_mul ... ok
test tests::test_matrix_mul_dimensions ... ok
test tests::test_transpose ... ok
test tests::test_identity ... ok
test result: ok. 8 passed; 0 failed; 0 ignored
$ cargo run --example demo
Compiling const-matrix v0.1.0
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/examples/demo`
=== Const Generic Matrix Demo ===
Creating a 3x2 matrix:
Matrix<3, 2> [
[ 1.0000, 2.0000]
[ 3.0000, 4.0000]
[ 5.0000, 6.0000]
]
Creating a 2x4 matrix:
Matrix<2, 4> [
[ 1.0000, 2.0000, 3.0000, 4.0000]
[ 5.0000, 6.0000, 7.0000, 8.0000]
]
Multiplying 3x2 * 2x4 = 3x4:
Matrix<3, 4> [
[ 11.0000, 14.0000, 17.0000, 20.0000]
[ 23.0000, 30.0000, 37.0000, 44.0000]
[ 35.0000, 46.0000, 57.0000, 68.0000]
]
Dimension mismatch would be caught at compile time!
// let bad = matrix_3x2 * matrix_3x4; // Won't compile!
4. Solution Architecture
4.1 High-Level Design
+-----------------------------------------------------------+
| Matrix<R, C> |
+-----------------------------------------------------------+
| Compile-Time Constants: |
| - R: Number of rows (const generic) |
| - C: Number of columns (const generic) |
+-----------------------------------------------------------+
| Runtime Data: |
| - data: [[f64; C]; R] (fixed-size nested array) |
+-----------------------------------------------------------+
| Operations (Trait Implementations): |
| - Add<Matrix<R,C>> -> Matrix<R,C> |
| - Sub<Matrix<R,C>> -> Matrix<R,C> |
| - Mul<Matrix<C,P>> -> Matrix<R,P> <-- KEY! |
| - Mul<f64> -> Matrix<R,C> (scalar) |
+-----------------------------------------------------------+
4.2 Key Components
- Core Struct:
Matrix<const R: usize, const C: usize> - Storage:
[[f64; C]; R]- row-major nested array - Operator Traits:
Add,Sub,Mul,Index,IndexMut - Utility Methods:
new(),zeros(),ones(),identity(),transpose() - Display:
Debugimplementation for pretty-printing
4.3 Data Structures
/// A fixed-size matrix with dimensions known at compile time.
#[derive(Clone, Copy, PartialEq)]
pub struct Matrix<const R: usize, const C: usize> {
data: [[f64; C]; R],
}
// Memory layout for Matrix<3, 4>:
// +------+------+------+------+
// | a00 | a01 | a02 | a03 | <- Row 0
// +------+------+------+------+
// | a10 | a11 | a12 | a13 | <- Row 1
// +------+------+------+------+
// | a20 | a21 | a22 | a23 | <- Row 2
// +------+------+------+------+
// Contiguous in memory: [a00, a01, a02, a03, a10, a11, ...]
4.4 Algorithm Overview
Matrix Multiplication Algorithm:
For Matrix<R1, C1> * Matrix<C1, C2> = Matrix<R1, C2>:
result[i][j] = sum(left[i][k] * right[k][j]) for k in 0..C1
Example: Matrix<2,3> * Matrix<3,2> = Matrix<2,2>
Left (2x3) Right (3x2) Result (2x2)
[a b c] [x u] [ax+bz+cy au+bw+cv]
[d e f] * [z w] = [dx+ez+fy du+ew+fv]
[y v]
5. Implementation Guide
5.1 Development Environment Setup
# Create project
cargo new --lib const-matrix
cd const-matrix
# Add dev dependencies for testing
cargo add --dev criterion proptest trybuild
Cargo.toml:
[package]
name = "const-matrix"
version = "0.1.0"
edition = "2021"
[dev-dependencies]
criterion = "0.5"
proptest = "1.4"
trybuild = "1.0"
[[bench]]
name = "matrix_bench"
harness = false
5.2 Project Structure
const-matrix/
├── Cargo.toml
├── src/
│ └── lib.rs # Main library code
├── examples/
│ └── demo.rs # Usage demonstration
├── benches/
│ └── matrix_bench.rs # Performance benchmarks
└── tests/
├── integration.rs # Integration tests
└── ui/ # Compile-fail tests
├── add_mismatch.rs
└── mul_mismatch.rs
5.3 The Core Question You’re Answering
“Can I force the compiler to understand linear algebra?”
Yes. By moving values (like 3 and 2) into the type system, the compiler’s trait solver becomes a dimensional analysis engine. Every matrix operation is verified before your program runs.
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Const Generics basics
- How to define
struct Matrix<const R: usize, const C: usize> - Book Reference: “Programming Rust” Ch. 11
- How to define
- Monomorphization
- Why does the compiler generate a new version of your function for every different size?
- Book Reference: “Idiomatic Rust” Ch. 5
- Trait implementation for Generics
- How to implement
Mulonly for matrices where columns of A match rows of B
- How to implement
5.5 Questions to Guide Your Design
- Storage
- Should you use
Vec<T>or[T; R * C]? (Hint: Since R and C are const, an array is faster!)
- Should you use
- Operations
- How do you define the return type of a multiplication as
Matrix<R1, C2>?
- How do you define the return type of a multiplication as
- Bounds
- How do you handle cases where you need
R * Cto be calculated at compile time? (Hint:generic_const_exprsfeature on Nightly)
- How do you handle cases where you need
5.6 Thinking Exercise
The Cost of Freedom
If you have 100 different matrix sizes in your program, how does that affect the binary size? Compare this to a library where dimensions are stored as runtime integers.
Answer: Each unique Matrix<R, C> type generates its own code. 100 sizes means 100 copies of each method. This is the “monomorphization cost” - larger binaries but perfect optimization for each size. Runtime-sized matrices have one code copy but add branching and bounds checks.
5.7 Hints in Layers
Hint 1: The definition
struct Matrix<const R: usize, const C: usize> { data: [[f32; C]; R] }
Hint 2: Implementation
Use the impl block:
impl<const R: usize, const C: usize> Matrix<R, C> { ... }
Hint 3: Multiplication
The Mul trait implementation:
impl<const R1: usize, const C1: usize, const C2: usize>
Mul<Matrix<C1, C2>> for Matrix<R1, C1>
{
type Output = Matrix<R1, C2>;
...
}
Hint 4: Verification
Use compile-fail tests with trybuild crate to verify invalid operations don’t compile.
5.8 The Interview Questions They’ll Ask
-
“What is the difference between a generic type parameter and a const generic parameter?”
-
“How do const generics reduce runtime overhead?”
-
“What are the limitations of const generics in Stable Rust currently?”
-
“Can you explain ‘Type State’ pattern using const generics?”
-
“How does monomorphization affect binary size vs runtime performance?”
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Const Generics | “Programming Rust” | Ch. 11 |
| Matrix Math | “Math for Programmers” | Ch. 4 |
| Type-level logic | “Idiomatic Rust” | Ch. 5 |
| Zero-Cost Abstractions | “Rust for Rustaceans” | Ch. 1 |
5.10 Implementation Phases
Phase 1: Foundation (Day 1)
Goal: Create the struct and basic constructors.
#[derive(Clone, Copy, PartialEq)]
pub struct Matrix<const R: usize, const C: usize> {
data: [[f64; C]; R],
}
impl<const R: usize, const C: usize> Matrix<R, C> {
pub fn new(data: [[f64; C]; R]) -> Self {
Self { data }
}
pub fn zeros() -> Self {
Self { data: [[0.0; C]; R] }
}
pub fn ones() -> Self {
Self { data: [[1.0; C]; R] }
}
}
Checkpoint: Can you create Matrix::<3, 4>::zeros()?
Phase 2: Debug and Access (Day 2-3)
Goal: Make matrices printable and add element access.
impl<const R: usize, const C: usize> std::fmt::Debug for Matrix<R, C> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(f, "Matrix<{}, {}> [", R, C)?;
for row in &self.data {
write!(f, " [")?;
for (i, val) in row.iter().enumerate() {
if i > 0 { write!(f, ", ")?; }
write!(f, "{:8.4}", val)?;
}
writeln!(f, "]")?;
}
write!(f, "]")
}
}
Phase 3: Addition and Subtraction (Day 4-5)
Goal: Enable matrix_a + matrix_b syntax.
impl<const R: usize, const C: usize> std::ops::Add for Matrix<R, C> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
let mut result = [[0.0; C]; R];
for i in 0..R {
for j in 0..C {
result[i][j] = self.data[i][j] + rhs.data[i][j];
}
}
Matrix { data: result }
}
}
Phase 4: Multiplication with Dimension Checking (Day 6-8)
Goal: Matrix multiplication with compile-time dimension verification.
impl<const R1: usize, const C1: usize, const C2: usize>
std::ops::Mul<Matrix<C1, C2>> for Matrix<R1, C1>
{
type Output = Matrix<R1, C2>;
fn mul(self, rhs: Matrix<C1, C2>) -> Self::Output {
let mut result = [[0.0; C2]; R1];
for i in 0..R1 {
for j in 0..C2 {
for k in 0..C1 {
result[i][j] += self.data[i][k] * rhs.data[k][j];
}
}
}
Matrix { data: result }
}
}
5.11 Key Implementation Decisions
| Decision | Options | Recommended | Rationale |
|---|---|---|---|
| Element type | Generic T vs fixed f64 |
Start with f64, generalize later |
Simpler bounds |
| Storage | [[T;C];R] vs [T;R*C] |
[[T;C];R] |
Natural row access |
| Copy semantics | Copy vs move |
Copy for small matrices |
Ergonomic |
| Row vs column major | Row-major vs column-major | Row-major | C convention |
6. Testing Strategy
6.1 Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_zeros() {
let m: Matrix<3, 4> = Matrix::zeros();
assert_eq!(m.data, [[0.0; 4]; 3]);
}
#[test]
fn test_addition() {
let a = Matrix::new([[1.0, 2.0], [3.0, 4.0]]);
let b = Matrix::new([[5.0, 6.0], [7.0, 8.0]]);
let c = a + b;
assert_eq!(c.data, [[6.0, 8.0], [10.0, 12.0]]);
}
#[test]
fn test_multiplication_dimensions() {
let a: Matrix<3, 2> = Matrix::ones();
let b: Matrix<2, 4> = Matrix::ones();
let c: Matrix<3, 4> = a * b;
// Each element is sum of row_a * col_b = 1*1 + 1*1 = 2
assert_eq!(c.data[0][0], 2.0);
}
}
6.2 Integration Tests
// tests/integration.rs
use const_matrix::Matrix;
#[test]
fn test_chain_operations() {
let a: Matrix<2, 3> = Matrix::ones();
let b: Matrix<3, 2> = Matrix::ones();
let c: Matrix<2, 2> = Matrix::identity();
let result = (a * b) * c;
// (2x3) * (3x2) = (2x2), then (2x2) * (2x2) = (2x2)
assert_eq!(result.data[0][0], 3.0);
}
6.3 Property-Based Testing
use proptest::prelude::*;
proptest! {
#[test]
fn matrix_addition_commutative(
a00 in -1000.0..1000.0f64,
a01 in -1000.0..1000.0f64,
b00 in -1000.0..1000.0f64,
b01 in -1000.0..1000.0f64,
) {
let a = Matrix::new([[a00, a01]]);
let b = Matrix::new([[b00, b01]]);
prop_assert_eq!(a + b, b + a);
}
}
6.4 Compile-Fail Tests
Create tests that verify invalid operations don’t compile:
// tests/ui/mul_mismatch.rs
use const_matrix::Matrix;
fn main() {
let a: Matrix<3, 2> = Matrix::zeros();
let b: Matrix<5, 4> = Matrix::zeros();
let _ = a * b; // ERROR: no impl for Mul<Matrix<5, 4>>
}
// tests/compile_fail.rs
#[test]
fn compile_fail_tests() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/*.rs");
}
7. Common Pitfalls & Debugging
| Problem | Symptom | Root Cause | Fix |
|---|---|---|---|
| Type inference failure | “cannot infer R and C” | Missing type annotation | Use Matrix::<3,4>::zeros() or let m: Matrix<3,4> = ... |
| Stack overflow | Crash on large matrix | Too big for stack | Box large matrices: Box::new(Matrix::zeros()) |
| Confusing row/col | Wrong results | Index order confusion | Remember: data[row][col] |
| Wrong output type | Type mismatch in multiply | Forgot output is Matrix<R1, C2> |
Check multiplication signature |
| Copy trait missing | Cannot use matrix twice | Forgot #[derive(Copy)] |
Add Copy bound or clone |
| Bounds check overhead | Slower than expected | Using get() in hot loop |
Use direct indexing after validation |
Debugging Checklist
- Compilation errors about trait bounds: Check that your const generic parameters match exactly in trait impls
- “the trait bound is not satisfied”: Ensure your impl covers the right types
- Infinite monomorphization: Avoid recursive types with growing const params
- Stack overflow: Large matrices should use
Box<Matrix<R,C>>
8. Extensions & Challenges
Extension 1: Gaussian Elimination
impl<const N: usize> Matrix<N, N> {
/// Solve Ax = b using Gaussian elimination
pub fn solve(&self, b: Matrix<N, 1>) -> Option<Matrix<N, 1>> {
// Implementation: augment [A|b], forward elimination, back substitution
}
/// Calculate determinant
pub fn determinant(&self) -> f64 {
// Use row reduction, det = product of pivots
}
/// Calculate inverse (if exists)
pub fn inverse(&self) -> Option<Self> {
// Use Gauss-Jordan with identity augmented
}
}
Extension 2: Generic Element Type
use num_traits::{Zero, One, NumOps};
pub struct Matrix<T, const R: usize, const C: usize>
where
T: Copy + Zero + One + NumOps,
{
data: [[T; C]; R],
}
// Now works with f32, f64, i32, complex numbers, etc.
Extension 3: SIMD-Optimized Operations
#![feature(portable_simd)]
impl<const R: usize, const C: usize> Matrix<R, C>
where
[(); C % 4]:, // Ensure C is multiple of 4
{
pub fn add_simd(self, rhs: Self) -> Self {
// Use SIMD for 4 f64s at once
}
}
Extension 4: Strassen Algorithm
Implement Strassen’s matrix multiplication for O(n^2.807) complexity instead of O(n^3).
9. Real-World Connections
nalgebra
The industry-standard linear algebra library for Rust uses const generics:
use nalgebra::{SMatrix, SVector};
// Fixed-size matrix (compile-time dimensions)
type Mat3x3 = SMatrix<f64, 3, 3>;
// Your project teaches the fundamentals that nalgebra builds on
Graphics Programming (glam, cgmath)
Game engines need fast, fixed-size math:
use glam::Mat4; // 4x4 matrix, size known at compile time
// Transformations in 3D graphics:
let model = Mat4::from_rotation_y(angle);
let view = Mat4::look_at_rh(eye, target, up);
let projection = Mat4::perspective_rh(fov, aspect, near, far);
let mvp = projection * view * model; // Type-safe multiplication!
Machine Learning (burn, candle)
Neural network frameworks use const generics for tensor shapes:
// In modern ML frameworks:
type LayerWeights = Matrix<784, 128>; // MNIST input -> hidden layer
type LayerBias = Matrix<128, 1>;
// Shape mismatches caught at compile time!
10. Resources
10.1 Documentation
10.2 Crates
nalgebra- Full-featured linear algebrandarray- N-dimensional arraysglam- Game math librarynum-traits- Numeric trait bounds
10.3 Articles & Talks
11. Self-Assessment Checklist
- I can explain what const generics are and why they’re useful
- I can implement
struct Matrix<const R: usize, const C: usize> - I understand monomorphization and its tradeoffs
- I can implement operator traits (
Add,Mul) with type constraints - I understand why
Mul<Matrix<C1, C2>>returnsMatrix<R1, C2> - I can write compile-fail tests using
trybuild - I can explain the difference between Type State and const generics
- My implementation compiles with no warnings
- All tests pass (including compile-fail tests)
- I can answer all the interview questions confidently
12. Submission / Completion Criteria
Your project is complete when:
- Core Implementation
Matrix<R, C>struct with const generic parametersnew(),zeros(),ones()constructorsAdd,Sub,Multrait implementationsidentity()for square matricestranspose()returningMatrix<C, R>
- Testing
- Unit tests for all operations
- Compile-fail tests proving dimension mismatches don’t compile
- Property-based tests for mathematical properties
- Documentation
- All public items documented
- Example in
examples/demo.rs - README with usage instructions
- Verification
cargo testpassescargo clippyhas no warningscargo docgenerates clean documentation
Final Deliverable: A working library that catches matrix dimension errors at compile time, with comprehensive tests proving both correct operations and rejected invalid operations.