102 Comparison with Rust
Predica and Rust both reject unrestricted simultaneous mutation and detect use after ownership transfer statically. Their surface models differ because Rust is a systems programming language, whereas Predica delegates physical representation and calling conventions to its compiler.
| Meaning | Predica | Rust |
|---|---|---|
| Read-only access | inspect(s) with parameter String x |
inspect(&s) with parameter x: &String |
| Exclusive mutable access | modify(&s) with parameter &String x |
modify(&mut s) with parameter x: &mut String |
| Ownership transfer | consume(^s) with parameter ^String x |
consume(s) with parameter x: String |
| Copy into an owned parameter | consume(s) |
consume(s.clone()) for a non-Copy value |
Explicit ownership transfer
Predica places the explicit marker on ownership transfer:
consume(s); // copy; s remains initialised
consume(^s); // move; s becomes uninitialised
For a non-Copy Rust value, an ordinary by-value call transfers
ownership and an explicit clone preserves the caller's value:
consume(s); // move
consume(s.clone()); // clone
Predica therefore makes loss of ownership visible at the call site without requiring the reader to inspect the parameter declaration. Its unmarked form preserves the caller's variable. The trade-off is that an unmarked call to an owned parameter may perform a potentially expensive copy that is not visible in the call syntax.
Read-only access is the default
A plain Predica parameter is read-only and non-owning:
func Boolean empty(String x) =
x = "";
empty(s)
The source language does not specify whether the physical argument is passed by value or by const reference. CIR may choose a value for a cheap representation and a const reference for an expensive representation without changing the Predica declaration. Rust expresses the shared borrow in its type and call syntax.
Exclusive reference access
Predica's reference form corresponds most closely to a Rust mutable borrow:
proc append(&String target)
{
target += "!";
}
append(&s);
Both languages grant exclusive access and prohibit competing reads and writes for the duration of that access. Predica references are deliberately more restricted: they exist only as procedure parameters, last exactly for the call, and cannot be stored, returned, captured, or otherwise escape.
Rust references are general types with statically checked lifetimes. They can be stored and returned when their lifetime relationships permit it, and they support APIs such as borrowed slices, iterators, and zero-copy views. Predica avoids lifetime syntax and the associated analysis at the cost of not directly expressing those patterns.
Owned parameters
An owned Predica parameter is marked in its declaration:
func String appendGreeting(^String x)
{
x += "hi";
return x;
}
The owned parameter may be read, updated, or consumed. A caller may initialize it from a new value, copy an existing value, or explicitly transfer ownership:
var String a = appendGreeting("a");
var String b = appendGreeting(s);
var String c = appendGreeting(^s);
Rust separates ownership from mutability of the parameter binding. A parameter
of type String owns its value, while mut x: String
additionally permits mutation through the binding. Predica's ^String
x grants both ownership and permission to update or consume the
parameter. This is less fine-grained but requires fewer parameter concepts.
Functions and procedures
Predica combines its access rules with a distinction between pure functions and
effectful procedures. A function may have plain or owned parameters. Updating
an owned parameter is local state and does not make the function impure. An
exclusive & parameter is restricted to procedures because it permits
the callee to update a caller variable.
Rust has no corresponding language-level separation between pure functions and procedures. A Rust function may accept a mutable reference and update caller state without being declared as a different kind of operation.
Definite initialisation
Both languages reject reading a variable after its value has been moved. Predica describes the result directly as an uninitialised variable:
consume(^s);
inspect(s); // static error: s is uninitialised
s := "new value";
inspect(s); // valid
Assignment can reinitialize the same binding. Predica applies the same definite-initialisation analysis to variables declared without initializers, moved variables, branches, loops, and exceptional control flow.
Different design priorities
Rust exposes ownership, borrowing, mutability, and lifetimes in enough detail to support systems programming, long-lived zero-copy views, and precise control over memory representation. This expressiveness brings additional type-system and lifetime complexity.
Predica instead presents three application-level operations: read a value, temporarily grant exclusive access, or explicitly transfer ownership. It leaves physical borrowing, copying, moving, and representation choices to CIR. This gives a smaller source-language model but may introduce implicit copying and cannot directly express Rust's long-lived borrowed data structures.