Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ownership Inference

Decy analyzes C code to infer Rust ownership semantics from pointer usage patterns. This is the core challenge of C-to-Rust transpilation: C has one pointer type (T*), while Rust distinguishes between owned values, shared references, mutable references, and raw pointers.

Inference Rules

Decy applies the following heuristics to classify each pointer parameter:

C PatternInferred Rust TypeRationale
const T* read-only param&T or &[T]No mutation, no ownership
T* modified but not freed&mut TMutation without ownership
T* returned from mallocBox<T> or Vec<T>Caller owns the allocation
T* passed to freeOwned (consumed)Transfer of ownership
T** output parameter&mut Option<T>Caller receives ownership

Shared References

C

double vector_sum(const double* data, size_t len) {
    double sum = 0.0;
    for (size_t i = 0; i < len; i++) {
        sum += data[i];
    }
    return sum;
}

Rust

#![allow(unused)]
fn main() {
fn vector_sum(data: &[f64]) -> f64 {
    data.iter().sum()
}
}

The const qualifier on data combined with no free call tells decy that this is a borrowed, read-only reference. The separate len parameter merges into the slice type.

Mutable References

C

void normalize(double* data, size_t len) {
    double max = 0.0;
    for (size_t i = 0; i < len; i++) {
        if (data[i] > max) max = data[i];
    }
    for (size_t i = 0; i < len; i++) {
        data[i] /= max;
    }
}

Rust

#![allow(unused)]
fn main() {
fn normalize(data: &mut [f64]) {
    let max = data.iter().copied().fold(f64::NEG_INFINITY, f64::max);
    for x in data.iter_mut() {
        *x /= max;
    }
}
}

The pointer is modified in place but not freed, so decy infers &mut [f64].

Owned Values

C

double* linspace(double start, double end, size_t n) {
    double* result = malloc(n * sizeof(double));
    double step = (end - start) / (double)(n - 1);
    for (size_t i = 0; i < n; i++) {
        result[i] = start + step * (double)i;
    }
    return result;  // Caller must free
}

Rust

#![allow(unused)]
fn main() {
fn linspace(start: f64, end: f64, n: usize) -> Vec<f64> {
    let step = (end - start) / (n - 1) as f64;
    (0..n).map(|i| start + step * i as f64).collect()
}
}

The malloc followed by return tells decy the caller takes ownership. The natural Rust equivalent is Vec<f64>.

Lifetime Annotations

When decy detects that a returned pointer aliases an input, it generates lifetime annotations:

C

// Returns pointer into data -- NOT a new allocation
const double* find_max(const double* data, size_t len) {
    const double* max = &data[0];
    for (size_t i = 1; i < len; i++) {
        if (data[i] > *max) max = &data[i];
    }
    return max;
}

Rust

#![allow(unused)]
fn main() {
fn find_max(data: &[f64]) -> &f64 {
    data.iter()
        .max_by(|a, b| a.partial_cmp(b).unwrap())
        .unwrap()
}
}

Decy recognizes that the returned pointer points into data rather than a new allocation. The Rust borrow checker enforces that the returned reference cannot outlive data.

Ambiguous Cases

When decy cannot determine ownership from usage patterns alone, it falls back to conservative choices and emits a warning:

WARN: Cannot infer ownership for `ctx` in process_data(Context* ctx).
      Defaulting to &mut Context. Review and adjust if needed.

These warnings are surfaced in the Batuta validation report, allowing developers to review and correct the small number of cases that require manual judgment.

Key Takeaways

  • Decy classifies C pointers into owned, shared, and mutable categories based on usage patterns (const, malloc, free, modification).
  • Separate length parameters merge into Rust slices automatically.
  • Returned pointers that alias inputs receive lifetime annotations.
  • Ambiguous cases produce warnings rather than silent incorrect translations.

Navigate: Table of Contents