Skip to content

← Smart Pointers and Interior Mutability step 11 of 26

Medium Primitives

RefCell<T>: borrowing checked at runtime

Repair an operation processor and implement a borrow-state probe. Both use RefCell<Vec<i64>>, which allows checked access to the vector through a shared reference to the cell.

pub fn apply_ops(initial: Vec<i64>, ops: Vec<String>) -> Result<Vec<i64>, String>
pub fn safe_probe(v: Vec<i64>) -> Vec<bool>

Process the operations

Put initial in a RefCell, apply operations in order and return the final vector in Ok. Stop at the first error and return its message in Err. Inputs keep multiplication and intermediate sums within the i64 range.

Split each operation with split_whitespace(). Extra surrounding or repeated whitespace is allowed, but command names are case-sensitive and the number of tokens must match. Numeric arguments must parse as i64. A malformed command or number returns bad op: <op>, preserving the original operation string after the prefix.

Operation Behavior
push <n> Append n.
pop Remove the last value; return pop on empty if none exists.
scale <n> Multiply each value by n.
append_sum Append the current sum, or 0 for an empty vector.
mirror Append a reversed copy of the current values. Empty input stays empty.
require <n> Continue if the sum equals n; otherwise return require: want <n>, got <s>.

In a failed requirement, <n> and <s> are formatted numeric values. Starting from [1, 2, 3], mirror gives [1, 2, 3, 3, 2, 1]. Starting separately from [1, 2, 3], append_sum gives [1, 2, 3, 6].

The harness reports an empty error string on success. On failure, it reports the error and an empty result vector, rather than partial operation state. It runs safe_probe independently on the separate probe input.

Probe six borrow states

Put v in a new cell and return six booleans from this sequence. Do not modify its values or trigger a borrow panic.

  1. Take a shared guard a. Record whether the vector is empty.
  2. While holding a, record whether try_borrow() succeeds.
  3. While holding a, record whether try_borrow_mut() succeeds.
  4. Drop a, then take a mutable guard b. Record whether every value is nonnegative. This is true for an empty vector.
  5. While holding b, record whether try_borrow() succeeds.
  6. Drop b, then record whether try_borrow_mut() succeeds.

Each successful trial borrow should be released before the next step. Calling .is_ok() without retaining the result's guard does this. For example, an empty input produces [true, true, false, true, false, true].

Borrow guards enforce access at runtime

borrow() returns a Ref guard for shared access. borrow_mut() returns a RefMut guard for mutable access. A new shared borrow is incompatible with an active mutable borrow; a new mutable borrow requires no active borrows. Dropping a guard releases the access it represents.

The panicking methods reject conflicts with a panic. try_borrow() and try_borrow_mut() instead return an error, which is useful when a conflict is an expected outcome, as in safe_probe. These checks supplement Rust's compile-time rules; they do not turn those rules off. Exact panic wording and the cell's internal representation are not part of this exercise.

Cell<Vec<i64>> is also usable, for example by taking the vector out, modifying it and putting it back. RefCell is useful here because it lets callers access the contained vector through borrow guards.

Return the guard from peek

The starter tries to return a reference obtained through a temporary guard:

fn peek(cell: &RefCell<Vec<i64>>) -> &Vec<i64> {
    &cell.borrow()
}

This produces E0515. The returned reference would outlast the guard that keeps its shared borrow active. Return Ref<'_, Vec<i64>> instead, with cell.borrow() as the body, and import Ref from std::cell.

The caller can read the vector through the returned guard and controls when that guard is dropped. Returning owned data or doing work inside a callback are other API designs, but this exercise uses a guard-returning helper.

End the read before starting the write

Fixing peek exposes a conflict in append_sum:

data.borrow_mut().push(peek(&data).iter().sum());

The method receiver is evaluated before its argument. The mutable guard therefore exists when peek requests a shared borrow of the same cell. Compute the sum in a separate statement, then acquire the mutable guard to append it. The temporary read guard ends before the write begins.

The starter's mirror arm has a similar conflict:

for x in peek(&data).iter() {
    data.borrow_mut().push(*x);
}

The loop keeps the shared guard alive while iterating. For nonempty input, the first attempted mutable borrow panics. For empty input, the loop body does not run. Even without that borrow conflict, forward iteration would not produce the reversed copy the operation requires.

Collect the reversed values into an owned vector in one statement, then extend the cell's vector in a second statement. If you bind a read guard separately, copying values does not itself drop that guard: explicitly drop it or end its scope before writing.

Thread-safety is a separate property

RefCell<T> can be moved to another thread when T: Send, but it is not Sync. Constructing Arc<RefCell<T>> can compile; attempting to share that combination between threads fails the required thread-safety bounds. Arc provides shared ownership, not synchronization of its contents. A mutex is one option when threads need coordinated mutable access.

Check operation results, exact error text and the six probe values. Passing requires compilation, correct behavior and Clippy with warnings treated as errors.