Workspace IndexAlgorithms › Day 30

Rust Ownership, the Borrow Checker (NLL), and Workaround Patterns TODO

Algorithms · Day 30 / 100 · B. Compilers, Runtimes & VMs (Day 20-35)

Concept

Rust's ownership model gives every value a single unique owner, and the owner freeing its resources when it goes out of scope guarantees memory safety at compile time without a GC. The borrow checker adds to this a rule that disallows aliasing and mutation at the same time, enforcing that at any given moment there are either multiple immutable references or exactly one mutable reference, never both. NLL (Non-Lexical Lifetimes) computes a borrow's valid range not as the entire lexical scope but as the point in the control-flow graph where that reference is last used, letting code that's actually safe but used to be rejected compile. Even so, there are structures the checker can't prove safe — self-referential structures, cyclic graphs, shared mutable state — and for those you fall back to an arena that uses indices instead of references, Rc and RefCell that push the check to runtime, per-field split borrows, or, as a last resort, unsafe encapsulated behind a safe API.

When you build a node, an indexer, or a ZK tool in Rust, a large share of the time goes not into logic but into fighting the borrow checker, and knowing the workaround patterns lets you sidestep that fight at the design stage.

Code & Formula

# Rust 소유권·차용 검사기 우회 패턴 — RefCell류 런타임 검사와 arena(인덱스) 패턴을 파이썬으로 흉내낸다.

class BorrowError(Exception):
    pass

class RefCell:
    # aliasing XOR mutation 규칙(불변 차용 다수 OR 가변 차용 단 하나)을
    # 컴파일 타임이 아니라 런타임에 검사로 옮긴 패턴
    def __init__(self, value):
        self._value = value
        self._shared_borrows = 0
        self._mut_borrowed = False

    def borrow(self):
        if self._mut_borrowed:
            raise BorrowError("이미 가변 차용 중인데 불변 차용 시도")
        self._shared_borrows += 1
        return self._value

    def release_borrow(self):
        self._shared_borrows -= 1

    def borrow_mut(self):
        if self._mut_borrowed or self._shared_borrows > 0:
            raise BorrowError("다른 차용이 있는데 가변 차용 시도")
        self._mut_borrowed = True

    def set(self, value):
        if not self._mut_borrowed:
            raise BorrowError("가변 차용 없이 값 변경 시도")
        self._value = value
        self._mut_borrowed = False

cell = RefCell(10)
v1, v2 = cell.borrow(), cell.borrow()      # 불변 차용은 여러 개 동시에 허용
print("동시 불변 차용:", v1, v2)
try:
    cell.borrow_mut()                      # 불변 차용이 살아있는데 가변 차용 시도 -> 위반
except BorrowError as e:
    print("차단됨:", e)
cell.release_borrow(); cell.release_borrow()
cell.borrow_mut()
cell.set(20)
print("가변 차용 해제 후 값 변경 성공:", cell._value)

# arena(인덱스) 패턴: 참조 대신 정수 인덱스로 구조를 표현해 차용 문제 자체를 피한다
class Arena:
    def __init__(self):
        self.nodes = []  # (value, children_indices)

    def add(self, value, children=()):
        self.nodes.append((value, list(children)))
        return len(self.nodes) - 1  # 인덱스를 "참조"처럼 반환

    def sum_subtree(self, idx):
        value, children = self.nodes[idx]
        return value + sum(self.sum_subtree(c) for c in children)

arena = Arena()
leaf1 = arena.add(1)
leaf2 = arena.add(2)
root = arena.add(10, children=[leaf1, leaf2])
print("arena 트리 합:", arena.sum_subtree(root))

Exercise

Build a reference-based tree or linked list, deliberately run into a compile failure, then reimplement the same structure once with Vec-index-based references and once with Rc<RefCell<..>>, and note what moved from a compile-time check to a runtime check.

Practical Connection

Ownership and aliasing rules remain a valid design principle outside of Rust too — they're directly useful for reframing concurrency bugs caused by shared mutable state in Go or TypeScript around the question "who owns this data?"

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

Rust 소유권·차용 검사기 내부(NLL)와 우회 패턴 TODO

Algorithms · Day 30 / 100 · B. 컴파일러·런타임·VM (Day 20–35)

개념

Rust의 소유권은 모든 값에 유일한 소유자를 두고 소유자가 스코프를 벗어날 때 자원을 해제해, GC 없이 컴파일 타임에 메모리 안전을 보장하는 규칙이다. 차용 검사기는 여기에 aliasing과 mutation을 동시에 허용하지 않는 규칙을 더해, 같은 시점에 다수의 불변 참조 또는 단 하나의 가변 참조만 존재하도록 강제한다. NLL(Non-Lexical Lifetimes)은 차용의 유효 구간을 렉시컬 스코프 끝까지가 아니라 제어 흐름 그래프상 그 참조가 마지막으로 쓰이는 지점까지로 계산해, 안전한데도 거부되던 코드를 통과시킨다. 그럼에도 검사기가 증명하지 못하는 구조(자기 참조, 순환 그래프, 공유 가변 상태)가 남고, 이때는 참조 대신 인덱스를 쓰는 arena 방식, Rc와 RefCell로 검사를 런타임으로 옮기는 방식, 필드별 분리 차용, 마지막 수단으로 unsafe를 안전한 API 뒤에 캡슐화하는 방식을 쓴다.

Rust로 노드, 인덱서, ZK 도구를 만들면 시간의 상당 부분이 로직이 아니라 차용 검사기와의 싸움에 들어가고, 우회 패턴을 알면 설계 단계에서 그 싸움 자체를 피할 수 있다.

코드 · 수식

# Rust 소유권·차용 검사기 우회 패턴 — RefCell류 런타임 검사와 arena(인덱스) 패턴을 파이썬으로 흉내낸다.

class BorrowError(Exception):
    pass

class RefCell:
    # aliasing XOR mutation 규칙(불변 차용 다수 OR 가변 차용 단 하나)을
    # 컴파일 타임이 아니라 런타임에 검사로 옮긴 패턴
    def __init__(self, value):
        self._value = value
        self._shared_borrows = 0
        self._mut_borrowed = False

    def borrow(self):
        if self._mut_borrowed:
            raise BorrowError("이미 가변 차용 중인데 불변 차용 시도")
        self._shared_borrows += 1
        return self._value

    def release_borrow(self):
        self._shared_borrows -= 1

    def borrow_mut(self):
        if self._mut_borrowed or self._shared_borrows > 0:
            raise BorrowError("다른 차용이 있는데 가변 차용 시도")
        self._mut_borrowed = True

    def set(self, value):
        if not self._mut_borrowed:
            raise BorrowError("가변 차용 없이 값 변경 시도")
        self._value = value
        self._mut_borrowed = False

cell = RefCell(10)
v1, v2 = cell.borrow(), cell.borrow()      # 불변 차용은 여러 개 동시에 허용
print("동시 불변 차용:", v1, v2)
try:
    cell.borrow_mut()                      # 불변 차용이 살아있는데 가변 차용 시도 -> 위반
except BorrowError as e:
    print("차단됨:", e)
cell.release_borrow(); cell.release_borrow()
cell.borrow_mut()
cell.set(20)
print("가변 차용 해제 후 값 변경 성공:", cell._value)

# arena(인덱스) 패턴: 참조 대신 정수 인덱스로 구조를 표현해 차용 문제 자체를 피한다
class Arena:
    def __init__(self):
        self.nodes = []  # (value, children_indices)

    def add(self, value, children=()):
        self.nodes.append((value, list(children)))
        return len(self.nodes) - 1  # 인덱스를 "참조"처럼 반환

    def sum_subtree(self, idx):
        value, children = self.nodes[idx]
        return value + sum(self.sum_subtree(c) for c in children)

arena = Arena()
leaf1 = arena.add(1)
leaf2 = arena.add(2)
root = arena.add(10, children=[leaf1, leaf2])
print("arena 트리 합:", arena.sum_subtree(root))

연습

참조 기반 트리나 연결 리스트를 만들어 일부러 컴파일 실패를 겪은 뒤, 같은 구조를 Vec 인덱스 기반과 Rc<RefCell<..>> 기반으로 각각 다시 구현하고 무엇이 컴파일 타임 검사에서 런타임 검사로 옮겨갔는지 정리하라.

실무 · Verex 연결

소유권과 aliasing 규칙은 언어를 떠나서도 유효한 설계 원칙이라, Go나 TypeScript에서 공유 가변 상태 때문에 생기는 동시성 버그를 "이 데이터의 소유자는 누구인가"라는 질문으로 재정리하는 데 그대로 쓰인다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 29. GC 심화31. 메모리 할당자 설계 →