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.