The 11 Requirements¶
MVL enforces eleven properties at compile time. If your code compiles, it satisfies all eleven. No runtime checks needed for these categories — they are structurally impossible to violate.
Overview¶
| # | Requirement | What it prevents | Key mechanism |
|---|---|---|---|
| 1 | Type safety | Impossible states | Algebraic data types |
| 2 | Memory safety | Use-after-free, buffer overflow | Ownership + borrowing |
| 3 | Exhaustive matching | Unhandled cases | Match completeness |
| 4 | Null elimination | Null pointer dereference | Option[T] only |
| 5 | Error visibility | Silent error swallowing | Result[T, E] required |
| 6 | Ownership | Double-free, resource leaks | Linear types |
| 7 | Effect tracking | Hidden side effects | Effect signatures |
| 8 | Termination | Infinite loops | total functions |
| 9 | Data race freedom | Concurrent access bugs | Actor isolation |
| 10 | Refinement types | Out-of-range values | where clauses |
| 11 | Information flow | Secret/tainted data leaks | IFC labels |
Requirement 1: Type Safety (ADTs)¶
What it prevents: Invalid state representations, type confusion, stringly-typed code.
Mechanism: Algebraic data types (sum types + product types) model exactly the states that can exist. No sentinel values, no magic numbers, no invalid combinations.
// Sum type: exactly one of these
enum PaymentStatus {
Pending,
Authorized { auth_code: String },
Captured { amount: Int },
Failed { reason: String },
}
// The compiler knows all variants. No "unknown status" bugs.
fn handle(status: PaymentStatus) -> String {
match status {
Pending => "waiting",
Authorized { auth_code } => "auth: ".concat(auth_code),
Captured { amount } => "captured: ".concat(amount.to_string()),
Failed { reason } => "failed: ".concat(reason),
}
}
What it replaces: Stringly-typed status fields, integer codes with magic values, nullable fields that "shouldn't be null in this state."
Requirement 2: Memory Safety¶
What it prevents: Use-after-free, double-free, buffer overflows, dangling pointers.
Mechanism: Ownership and borrowing. Every value has exactly one owner. References borrow without ownership transfer. The compiler tracks lifetimes.
fn process(data: String) -> Unit {
let owned: String = data; // ownership transferred
consume(owned); // ownership transferred to consume()
// println(owned); // compile error: owned moved
}
fn read_only(data: ref String) -> Int {
data.len() // borrow, no ownership transfer
}
What it replaces: Garbage collection overhead, manual malloc/free, reference counting cycles.
Requirement 3: Exhaustive Matching¶
What it prevents: Unhandled cases, forgotten enum variants, incomplete switch statements.
Mechanism: match expressions must cover all variants. Adding a new variant to an enum causes compile errors everywhere it's matched — forcing you to handle it.
enum Direction { North, South, East, West }
fn to_vector(d: Direction) -> (Int, Int) {
match d {
North => (0, 1),
South => (0, -1),
East => (1, 0),
// compile error: non-exhaustive match, missing: West
}
}
What it replaces: Default cases that swallow unexpected values, "this should never happen" runtime panics.
Requirement 4: Null Elimination¶
What it prevents: Null pointer dereference — the "billion dollar mistake."
Mechanism: No null. Optional values use Option[T] — either Some(value) or None. The compiler forces you to handle both cases.
fn find_user(id: Int) -> Option[User] {
// returns Some(user) or None
}
fn greet(id: Int) -> String {
match find_user(id) {
Some(user) => "Hello, ".concat(user.name),
None => "User not found",
}
// Cannot forget to handle None — won't compile
}
What it replaces: Null checks scattered throughout code, NullPointerException, undefined behavior.
Requirement 5: Error Visibility¶
What it prevents: Silent error swallowing, ignored return codes, exceptions that propagate invisibly.
Mechanism: Fallible operations return Result[T, E]. Errors must be explicitly handled or propagated with ?. No exceptions.
fn read_config(path: String) -> Result[Config, IoError] ! FileRead {
let content: String = read_file(path)?; // propagates error
parse_config(content)
}
fn main() -> Unit ! FileRead + Console {
match read_config("app.toml") {
Ok(config) => println("loaded"),
Err(e) => println("error: ".concat(e.message)),
}
}
What it replaces: Try/catch blocks, unchecked exceptions, errno that nobody checks.
Requirement 6: Ownership (Linearity)¶
What it prevents: Double-free, resource leaks, use-after-close.
Mechanism: Linear types ensure resources are used exactly once. File handles, connections, locks — if you open it, you must close it, exactly once.
fn with_file(path: String) -> Result[Unit, IoError] ! FileRead {
let file: File = open(path)?; // file opened
let content: String = read(file); // file consumed (closed after read)
// read(file); // compile error: file already consumed
Ok(())
}
What it replaces: try-with-resources, defer statements, RAII (which still allows use-after-move in some languages).
Requirement 7: Effect Tracking¶
What it prevents: Hidden side effects, impure functions pretending to be pure, untraceable I/O.
Mechanism: Effects declared in function signatures with !. A function that does console I/O must declare ! Console. Effects propagate — callers must declare effects of callees.
fn pure_add(a: Int, b: Int) -> Int {
a + b // no effects — pure function
}
fn greet(name: String) -> Unit ! Console {
println("Hello, ".concat(name)) // Console effect
}
fn main() -> Unit ! Console + FileRead {
greet("world"); // must declare Console
let data = read_file("x.txt")?; // must declare FileRead
}
What it replaces: "I/O monad" patterns, implicit global state, functions that secretly write to disk.
Requirement 8: Termination¶
What it prevents: Infinite loops, non-terminating recursion in critical paths.
Mechanism: Functions marked total must provably terminate. The compiler verifies recursion has decreasing arguments and loops have bounded iterations.
total fn factorial(n: Int where n >= 0) -> Int {
if n == 0 { 1 } else { n * factorial(n - 1) }
// Compiler proves: n decreases each call, base case exists
}
total fn sum_list(xs: List[Int]) -> Int {
match xs {
[] => 0,
[head, ..tail] => head + sum_list(tail),
}
// Compiler proves: list shrinks each call
}
What it replaces: "Trust me, this terminates" comments, timeout-based recovery, unbounded retry loops.
Requirement 9: Data Race Freedom¶
What it prevents: Concurrent access to shared mutable state, race conditions, heisenbugs.
Mechanism: Actor model. Mutable state lives inside actors. Communication via message passing only. No shared memory between actors.
actor Counter {
state count: Int = 0
msg increment() -> Unit {
self.count = self.count + 1
}
msg get() -> Int {
self.count
}
}
fn main() -> Unit ! Spawn {
let counter: Counter = spawn Counter {};
counter.increment(); // message send, not direct access
counter.increment();
let value: Int = counter.get();
}
What it replaces: Mutexes, locks, atomic operations, "synchronized" blocks, lock-free data structures.
Requirement 10: Refinement Types¶
What it prevents: Out-of-range values, invalid arguments, violated preconditions.
Mechanism: where clauses on types. The compiler proves constraints at compile time using an SMT solver.
fn divide(a: Int, b: Int where b != 0) -> Int {
a / b // division by zero impossible
}
fn percentage(p: Int where p >= 0, p <= 100) -> String {
p.to_string().concat("%")
}
fn main() -> Unit ! Console {
println(divide(10, 2)); // OK: compiler proves 2 != 0
// println(divide(10, 0)); // compile error: 0 violates b != 0
let x: Int = 50;
println(percentage(x)); // OK if compiler can prove 0 <= x <= 100
}
What it replaces: Runtime validation, assert statements, "throws IllegalArgumentException."
Requirement 11: Information Flow Control¶
What it prevents: Secret data leaking to logs, tainted input reaching SQL queries, PII exposed to analytics.
Mechanism: IFC labels on types. Data carries its security label. The compiler tracks information flow and prevents unauthorized release.
label Secret
label Tainted
relabel declassify: Secret -> _ audit // requires audit tag
relabel sanitize: Tainted -> _ // explicit trust boundary
fn process_password(pw: Secret[String]) -> Unit ! Console {
// println(pw); // compile error: Secret cannot flow to Console
let hash: String = relabel declassify(hash(pw), "AUTH-001");
println("hash: ".concat(hash)); // OK: declassified with audit trail
}
fn query(input: Tainted[String]) -> String ! Database {
// execute("SELECT * FROM users WHERE name = " + input); // compile error
let safe: String = relabel sanitize(validate(input));
execute("SELECT * FROM users WHERE name = ".concat(safe))
}
What it replaces: Manual taint tracking, security reviews, "don't log passwords" policies.
The Compound Effect¶
Each requirement is valuable alone. Together, they compound:
- Type safety + exhaustive matching = impossible states don't compile
- Null elimination + error visibility = every failure path is explicit
- Ownership + effect tracking = resource management is provable
- Refinement + IFC = security properties are types, not policies
The eleven requirements are not a checklist. They are an integrated system where each property reinforces the others.
Code that compiles is well-formed. Tests verify it does the right thing — not that it handles nulls, not that it avoids races, not that it tracks secrets. Those are already proven.