Hone logo
Hone
Problems

Rust Borrow Checker Simulation

Rust's borrow checker is a powerful tool that enforces memory safety without a garbage collector. It ensures that references are always valid and that data is not mutated unexpectedly. This challenge asks you to simulate a simplified version of Rust's borrow checker to understand its core principles.

Problem Description

Your task is to create a BorrowChecker struct in Rust that can track the ownership and borrowing of "data" (represented by a simple i32 value for this exercise). The BorrowChecker should enforce the following rules, mimicking Rust's borrowing rules:

  • One Mutable Borrow or Multiple Immutable Borrows: At any given time, a piece of data can either have one mutable reference or any number of immutable references, but not both.
  • Borrow Lifetimes: References are valid only as long as the data they point to is valid.
  • No Dangling References: A reference cannot outlive the data it refers to.

You will need to implement methods to:

  1. Create data: Introduce a new piece of data into the checker's management.
  2. Borrow mutably: Obtain a mutable reference to a piece of data. This should fail if there are any existing immutable or mutable borrows.
  3. Borrow immutably: Obtain an immutable reference to a piece of data. This should fail if there is an existing mutable borrow.
  4. Drop data/references: Explicitly signify when data or a reference is no longer needed. This is crucial for allowing new borrows.

The BorrowChecker will store its state internally and return Result types to indicate success or failure of borrow operations, similar to how Rust's compiler would.

Examples

Example 1: Successful Immutable Borrows

let mut checker = BorrowChecker::new();

// Create data with ID 0
checker.create_data(0, 10).unwrap();

// Borrow data 0 immutably
let borrow1 = checker.borrow_immutable(0).unwrap();
println!("Immutable borrow 1: {}", borrow1); // Output: Immutable borrow 1: 10

// Borrow data 0 immutably again
let borrow2 = checker.borrow_immutable(0).unwrap();
println!("Immutable borrow 2: {}", borrow2); // Output: Immutable borrow 2: 10

// Data 0 is still valid
println!("Data 0 value: {}", checker.get_data_value(0).unwrap()); // Output: Data 0 value: 10

// Clean up borrows
checker.drop_immutable_borrow(0, &borrow1);
checker.drop_immutable_borrow(0, &borrow2);

// Clean up data
checker.drop_data(0).unwrap();

Explanation: Initially, data with ID 0 and value 10 is created. Two immutable borrows are successfully acquired because there are no active mutable borrows. The data can be accessed. Finally, the borrows and then the data are dropped.

Example 2: Failed Mutable Borrow

let mut checker = BorrowChecker::new();

// Create data with ID 1
checker.create_data(1, 20).unwrap();

// Borrow data 1 immutably
let borrow1 = checker.borrow_immutable(1).unwrap();
println!("Immutable borrow 1: {}", borrow1); // Output: Immutable borrow 1: 20

// Attempt to borrow data 1 mutably - should fail
let mutable_borrow_result = checker.borrow_mutable(1);
assert!(mutable_borrow_result.is_err());
println!("Mutable borrow attempt result: {:?}", mutable_borrow_result); // Output: Mutable borrow attempt result: Err("Data 1 is already immutably borrowed")

// Clean up borrow
checker.drop_immutable_borrow(1, &borrow1);

// Clean up data
checker.drop_data(1).unwrap();

Explanation: Data with ID 1 is created. An immutable borrow is acquired. When a mutable borrow is attempted, it fails because an immutable borrow is active.

Example 3: Successful Mutable Borrow After Immutable Drops

let mut checker = BorrowChecker::new();

// Create data with ID 2
checker.create_data(2, 30).unwrap();

// Borrow data 2 immutably
let borrow1 = checker.borrow_immutable(2).unwrap();

// Attempt mutable borrow - fails
let mutable_borrow_result = checker.borrow_mutable(2);
assert!(mutable_borrow_result.is_err());

// Drop the immutable borrow
checker.drop_immutable_borrow(2, &borrow1);

// Now, attempt mutable borrow - should succeed
let mut mutable_borrow = checker.borrow_mutable(2).unwrap();
*mutable_borrow = 35; // Mutate the data
println!("Mutated value: {}", *mutable_borrow); // Output: Mutated value: 35

// Clean up mutable borrow
checker.drop_mutable_borrow(2, &mutable_borrow);

// Clean up data
checker.drop_data(2).unwrap();

Explanation: Data with ID 2 is created. An immutable borrow is taken, preventing a mutable borrow. After the immutable borrow is dropped, a mutable borrow becomes possible, allowing the data to be modified.

Constraints

  • Data IDs will be non-negative integers (u32).
  • Data values will be i32.
  • The BorrowChecker should support a maximum of 100 distinct data items simultaneously.
  • The BorrowChecker should manage its state efficiently. Operations like creating, borrowing, and dropping should ideally be O(1) on average, assuming data IDs are manageable.
  • The simulation does not need to handle complex generic types or lifetimes beyond what's implied by the borrow checker's rules.

Notes

  • You will need to design the internal data structures for BorrowChecker to track the state of each data item (e.g., whether it's free, immutably borrowed, or mutably borrowed, and how many immutable borrows exist).
  • Consider using HashMap for efficient lookup of data by ID.
  • The borrow_mutable and borrow_immutable methods should return a value that allows modification (for mutable) or reading (for immutable) of the data. You might need to return a wrapper struct or a reference within the Result.
  • The drop_data method signifies that the underlying data is being deallocated and should no longer be accessible.
  • The drop_immutable_borrow and drop_mutable_borrow methods are crucial for releasing borrows, allowing subsequent operations.
  • The error types returned by borrow_mutable and borrow_immutable should be informative. You can define your own enum for this.
Loading editor...
rust