Valen은 같은 객체를 가리키는 여러 참조로 읽기와 쓰기를 허용하면서, 해제된 데이터에 대한 참조 사용을 컴파일 시점에 차단하는 그룹 빌림(group borrowing) 기반 검사기를 구현함
- 컴파일러는 각 참조가 가리키는 소유권 경로를 추적함. 객체를 변경하면 그 객체 자체가 아니라 내부 데이터를 가리키는 참조를 무효화하고, 이후 사용을 오류로 처리함
- 함수 시그니처에 변경 가능한 경로를 명시해, 전체 객체를 읽으면서 특정 내부 객체만 수정할 수 있음. 가변성은 참조 타입이 아니라 함수의 속성이며, 빌림 참조는 한 종류만 존재함
Rust 상호운용을 위해 호출 중 데이터 접근의 유일성을 확인하는 일시적 유일성과, 반환 참조의 대략적인 위치를 나타내는 와일드카드 하위 경로를 도입함
참조 카운팅과 세대별 참조를 결합하는 설계도 추진 중임. 더 정밀한 변경 정보에 따른 최적화 가능성은 아직 전망이며, 타입이 안정적인 내부 데이터의 참조를 유지하는 예외 규칙은 미구현 상태임
목표와 현재 구현 범위
-
Valen의 목표는 Rust 빌림 검사 수준의 강력함과 참조 카운팅이나 가비지 컬렉션의 단순함과 유연성을 함께 확보하는 것임
-
이를 위해 세 가지 과제를 풀어야 함
-
동일 객체에 여러 읽기/쓰기 참조를 허용하는 더 느슨한 빌림 검사가 가능한가
-
객체끼리 자유롭게 참조하는 임의의 데이터 그래프에도 불변 빌림 참조를 만들 수 있는가
-
두 체계를 동시에 작동시킬 수 있는가
-
현재 설명의 중심은 첫 번째 과제임. 나머지는 Vale의 세대별 참조와 영역 빌림, 그룹 빌림, 2023년의 사고실험을 결합하는 방식으로 접근함
-
Vale의 불변 영역 빌림은 기존 데이터를 일시적으로 동결된 영역으로 추적해 그 안을 빌릴 수 있게 함
-
예제의 빌림 검사 동작은 구현되어 있지만, 일부 문법 편의 기능은 11월 도입 예정임
-
현재는 수동 역참조,
my_vec[0]
대신 my_vec.data[0]
, entity &Entity in world.entities[]
처럼 명시적인 매개변수 타입이 필요함
- 자동 역참조, 구조체 직접 인덱싱, 그룹에서 타입을 추론하는 문법은 예제의 가독성을 위해 미리 사용함
그룹 빌림의 출발과 다른 언어로의 확산
그룹 빌림은 Nick Smith가 설계했으며, 핵심은 컴파일러가 참조의 목적지를 기억하고 그 정보로 메모리 안전성을 검사하는 것임
- 처음에는 Mojo를 염두에 둔 설계였지만 채택되지 않았음. 이후 설명과 이점을 다듬어 공개한 이전 글을 계기로 여러 언어가 이를 받아들이기 시작함
- 언어마다 유연성을 원하는 이유가 다름.
Valen은 참조 카운팅과 세대별 참조, Carbon은 C++ 코드와 함께 작동하는 빌림 검사기를 지향함
공유와 변경의 충돌을 푸는 방식
- 기존 빌림 검사와 다른 메모리 관리 방식 사이에는 공유와 변경의 충돌이 있음
shared-xor-mutable
제약은 참조를 유지하는 동안 다른 경로를 통한 변경을 제한함
와 여러 장기 유지 원시 포인터, 참조 카운팅, 가비지 컬렉션은 다른 참조를 유지하면서도 객체를 변경하는 사용 방식을 허용하려 함
-
그룹 빌림은 제약의 초점을 공유와 변경의 양립 금지에서 해제 후 사용 방지로 이동시킴
-
참조가 존재한다는 이유만으로 변경을 금지하지 않고, 변경으로 무효화된 참조를 나중에 사용하는지 검사함
-
참조 대상이 다른 경로에서 변경될 수 있어야 하는지는 논쟁적인 선택이며, Valen은 사용자에게 선택권을 주려 함
-
이 충돌은 Vale의 과거 설계에도 영향을 줌. 세대별 참조를 범위 동안 붙잡아 두는 hybrid-generational memory
방식은 32개 버전을 시도한 뒤 포기했음
- 그룹 빌림은
컴파일 시점 검사로 실행 시점 비용 없는 안전성을 지향하지만, 어떤 언어든 완전한 무비용 안전성을 달성할 수 있는지는 별도 문제임. 무오버헤드 메모리 안전성에 관한 논의는 그룹 빌림에도 적용됨
허용하는 코드 패턴과 World 예제
여러 읽기/쓰기 참조를 허용하므로, 같은 리스트를 가리키는 ref_a와 ref_b를 만들고 두 참조 모두로 push할 수 있음
-
다음과 같은 패턴도 지원함
-
같은 객체를 가리키는 여러 함수 매개변수로 변경할 수 있음
-
객체 전체와 그 내부 객체를 매개변수로 받아 내부 객체만 변경할 수 있음
-
범위를 벗어날 때 원래 객체를 수정하는 ScopeGuard 형태의 롤백 구조체를 만들 수 있음
-
여러 구조체가 공통 하위 시스템에 대한 가변 참조를 보유할 수 있음
-
롤백 구조체 방식은 사용자 정의defer의 기반이 될 수도 있음
-
대표 예제는 전체 World를 읽으면서 Entity 하나를 변경하는 함수임
func step(world &World, entity in world.entities[] mut) {
entity.advance();
let collision = world.get_collision_for_entity(entity);
...
}
in은 해당 그룹의 원소를 가리키는 참조라는 뜻이며, world.entities[]는 world의 entities컬렉션 원소들을 뜻함
mut은 그 원소를 변경할 수 있다는 뜻이지, Rust의 &mut처럼 유일한 참조라는 뜻은 아님
-
컴파일러는 정확한 원소 번호가 아니라 어느 객체 그룹 안을 가리키는지를 앎
-
함수는 World 전체가 아니라 허용된 엔티티 부분만 변경할 수 있음
-
같은 작업을 C++로 쓰면 단순하지만 컴파일러가 메모리 안전성을 보장하지 않음. 일반 Rust 예제는 안전하지만 엔티티 참조를 반복해서 얻어야 하고, 각 인덱싱에 경계 검사와 패닉 가능성이 있음
-
GhostCell과 GhostToken을 사용하면 예제에서 C++와 같은 속도를 얻을 수 있지만 브랜드와 토큰을 관리해야 함
-
다만 Rust 코드를 재배치하면 GhostCell 없이도 대체로 그룹 빌림과 같은 속도를 얻을 수 있음
-
데이터를 더 평평하게 재구성하면 빌림 검사가 쉬워지고 ECS식 순회에서 빨라질 수 있지만, 단일 소유권의 RAII 이점을 일부 잃을 수 있음
-
이질적인 트리를 여러 배열로 나누면 부모 삭제가 자식 자동 삭제로 이어지지 않음
-
병렬 컬렉션을 과도하게 나누면 ID 추적으로 캐시 미스가 늘어날 위험도 있음
-
특정 메모리 안전성 방식이 아키텍처 선택을 유도하는 문제는 그룹 빌림에도 일부 존재하며, 참조 카운팅과 세대별 참조를 결합하려는 이유 중 하나임
단일 소유권과 경로
-
검사기의 기반은 단일 소유권임. 값은 자신을 포함하는 객체, 배열, 스택 프레임 또는 전역에 소유됨
-
예를 들어 main의 스택 프레임이 Vec<Ship>을 소유하고, 벡터가 각 Ship, 각 선박이 Box<Engine>, 엔진이 fuel을 소유함
-
벡터 참조를 받는 foo는 벡터를 소유하지 않음
-
C에서도 malloc()부터 free()까지 해제 책임이 이동하는 경로를 추적할 수 있으며, Java나 C#에서도 생성부터 dispose나 cleanup까지 책임을 추적하는 사고방식이 가능함
-
같은 소유권 기반에 어떤 참조 체계를 얹는지에 따라 접근법이 달라짐
-
제한 없는 포인터와 참조를 더하면 C++, 세대별 참조와 영역 빌림을 더하면 Vale가 됨
-
배타성을 검사하는 참조를 더하면 Rust, 그룹 빌림과 추가 메커니즘을 더하면 Valen이 됨
경로(path) 는 소유권 계층 안의 데이터를 나타냄. 예를 들어 world, world.diameter
, world.entities,world.entities[],world.entities[].hp``가 있음
-
시작점은 지역 변수, 함수 매개변수 또는 그룹 매개변수임
.diameter``는 멤버, ``[]``는 컬렉션 원소, .WarpEngine` 같은 표기는 유니온의 변형을 나타냄
-
연관 그룹이나 다중 타입 그룹의 하위 타입 같은 확장도 있음
-
단일 소유권에서는 기본적으로 객체마다 하나의 경로가 있지만,
연관 그룹(Associated Group) 같은 기능을 쓰면 여러 경로로 접근할 수도 있음
&world.entities[0]에서 얻은 참조의 타입은 &Entity in world.entities[]처럼 목적지 경로를 포함함
-
이 경로는 Rust의 수명(lifetimes)과 구별해야 함. 수명은 명령 실행 구간에 더 가깝고, 경로는 데이터 위치를 나타냄
참조 무효화와 함수 시그니처
- 메모리 안전성 검사는 두 규칙으로 작동함
- 객체를 변경하면
**그 객체 내부를 가리키는 참조(reference)**를 무효화함
- 무효화된 참조를 사용하면 컴파일 오류를 냄
world.entities를 새 벡터로 교체하면 기존 원소를 가리키던 first_ref는 무효화됨. 이후 first_ref.hp를 읽으면 Used a borrow after invalidated 오류가 발생함.
객체 자체와 객체 내부의 참조를 구분하는 것이 중요함
vec.push(...)는 원소를 가리키는 elem_ref를 무효화하지만, 벡터 자체를 가리키는 vec_ref는 유지함
-
따라서
vec_ref로 다시 push할 수 있지만 기존 elem_ref를 사용할 수는 없음
-
Valen에는
**한 종류의 빌림 참조(borrow reference)**만 있으며, 가변성은 참조 타입의 일부가 아님
func push<T>(self &Vec<T> mut)는 func push<T>(self &Vec<T>) mut(self)의 문법적 축약임
mut(self)는 함수가 변경하는 경로를 표시함
-
호출 지점에서는 함수의 경로를 실제 인수에 대응시켜 무효화 범위를 계산함
vec.push(...)에서는 self가 vec이므로 그 내부 참조를 무효화함
step(world, entity)가 world.entities[]의 엔티티 내용만 변경한다면 엔티티 자체의 참조는 유지되어, 호출 뒤에도 entity.read_book()같은 접근이 가능함
-
경로를 기억하는 참조는 객체의 세대 ID를 기억하는 세대별 참조(generational reference)와 닮은 점이 있음. 그룹 빌림을 일종의
컴파일 시점 세대별 참조로 볼 수 있는지는 더 탐구할 여지가 있음
불변 영역 안의 가변 부분과 최적화 가능성
**불변 내부의 가변 부분(Mutable-in-Immutable)**은 넓은 읽기 접근과 좁은 쓰기 접근을 함께 표현함
-
World 전체를 가변으로 받지 않고 특정 엔티티에만 변경 권한을 줄 수 있음
-
함수가 예상하지 않은 World 부분을 바꾸지 못하도록 강제해 더 강한 API 계약을 만들 수 있음
-
세밀한 변경 정보는
최적화에도 도움이 될 가능성이 있음
-
두 함수가 읽기 영역만 공유하고 쓰기 영역은 겹치지 않는다는 사실을 알면 실행 순서를 바꾸거나 계산을 루프 밖으로 옮길 수 있음
-
이를 활용하려면 컴파일러 설계와 LLVM 수정이 필요할 수 있음. LLVM은 읽기 전용 범위보다
noalias
중심으로 처리하는 경향이 있음
-
이 가능성이 Valen의 C++나 Rust 대비 더 높은 속도를 보장하지는 않음
-
다른 언어에서도 추가 리팩터링이나
unsafe
로 같은 최적화를 얻을 수 있을 것으로 예상함
-
기대하는 주된 이점은
사용 편의성과 유연성임
Rust 연동을 위한 일시적 유일성
-
Valen은 Kotlin에서 Java를, C++에서 C를 호출하듯
Rust 코드를 자연스럽게 호출하는 것을 목표로 함
-
그룹 빌림과 Rust의 참조 규칙을 연결하기 위해
일시적 유일성(Temporary Uniqueness) 을 사용함
-
호출 중 해당 데이터에 도달할 수 있는 유일한 참조라면 Valen 참조를 Rust의
&mut
로 넘길 수 있음
-
여러 Valen 참조가 같은 객체를 가리킬 수 있다면 Rust의
&
로만 넘길 수 있음
-
자세한 경계 규칙은 Valen/Rust 경계의 메모리 안전성에서 확인할 수 있음
-
장기적으로는 Rust의
Vec<&mut Ship>
처럼 각 원소가 유일한 참조인 구조도 지원하려 함
unique(world.ships[])
같은 유일 그룹 표시로 해당 계층에 새 참조를 만드는 것을 제한하는 구상을 검토 중임
-
아직 추가 설계가 필요하며, 두 번째 참조 종류를 도입하면 빌림 참조가 하나뿐이라는 장점을 해칠 수 있음
Rust 반환 참조와 와일드카드 하위 경로
-
Rust 함수가 참조를 반환할 때 Valen은 그 참조의
정확한 내부 경로를 추론할 수 없음
-
예를 들어
Chest::gem(&self) -> &Gem
의 Rust 시그니처만으로는 Valen이 원하는 정확한 목적지 정보를 얻을 수 없음
-
이를 위해
와일드카드 하위 경로(Wildcard Descendant Paths) 를 도입함
...
는 특정 경로의 내부 어딘가를 가리킨다는 뜻임
-
예제의
in self.gem...
은 self.gem
내부 어딘가에 대한 참조를 나타냄
-
이 기능은 Rust 연동 외에도 시그처에서 내부 구현을 숨기고 라이브러리 API의 유연성과 향후 호환성을 높이는 데 유용할 수 있음
-
다만 비공개 그룹을 공개 이름으로 다시 노출하는
연관 그룹 같은 다른 기능으로도 같은 목적을 달성할 가능성이 있어 신중히 검토해야 함
참조 카운팅과 복잡한 데이터 그래프의 결합 구상
-
사용자가 경로를 명시하고 싶지 않을 때는
**참조 카운팅을 사용하는 **class
를 선택할 수 있게 하려 함
func step(world &World, entity in world.entities[] mut)
대신 func step(world World, entity Entity) mut
처럼 쓰는 구상임
-
참조 카운팅은 장기간 유지되는 참조, 관찰자, 침투형 연결 리스트 등 더 많은 패턴을 허용하며, 접근 중인 객체가 해제되지 않도록 함
-
반면
순환 참조 문제가 있고 선형 타입과 잘 맞지 않는 면이 있음
-
일반적인 작업에는 참조 카운팅, 복잡하거나 속도가 중요한 부분에는 빌림을 선택할 수 있도록 두 방식을 함께 지원하려 함
-
참조 카운팅과 빌림의 결합은
Rc<RefCell<T>>
에 RefCell
이 필요한 것과 같은 이유로 어려움. 해결 실마리는 2023년의 Arrrlang 사고실험에서 얻음
-
힙을 프로그램의 타입 수
N
개에 대응하는 전역 배열들로 간주함
-
그룹 빌림은 배열을 이해하므로, 서로 참조하는 참조 카운팅 객체들도 배열들의 집합으로 설명할 수 있다는 발상임
-
실제 객체를 배열로 배치할 필요는 없으며,
빌림 검사기에 그렇게 모델링하면 됨
-
원래 사고실험 자체에는 ID 추적과 경계 검사 비용이 있었음
-
이 결합에는 두 가지 구성 요소를 추가해야 함
다중 타입 그룹은 그룹마다 하나의 타입을 요구하는 기본 그룹 빌림과 달리, Vale 영역처럼 여러 타입을 포함함. 열거형에도 유용할 수 있음
실행 시점 그룹은 그 안을 가리키는 참조를 무효화하지 않음
-
Rust 일부의 안전성 형식 검증에 사용된 Iris도 내부적으로 이와 같은 방식으로 힙을 표현함
-
참조 카운팅과 세대별 참조를 결합하는 방식, 그룹 빌림 검사기의 구현에 관한 구체적인 설명은 후속으로 남아 있음.
Cell
, RefCell
, Mutex
없이 참조 카운팅을 사용하는 것도 목표 중 하나임
Vale와 Valen의 설계 차이
Valen은 Vale의 영역 빌림(scope borrowing) 대신 그룹 빌림(group borrowing)을, 확률적 세대별 참조(probabilistic generational reference) 대신 일반적인 세대별 참조(general generational reference)를 사용할 계획임
- Vale에 없던 Rust 상호운용,
Rc, comptime을 지원하려 하며, Vale에 있던 완전한 재현 실행(full reproducible execution)과 fearless FFI는 포함하지 않음
- 두 언어 모두 선형 타입(linear type)을 갖추고 있으며 문법은 아직 대체로 비슷함. Valen 문법은 앞으로 더 단순한 Rust에 가까워질 수도 있음
미구현 예외: 타입 안정성이 유지되는 내부 데이터
타입 안정성 예외(type-stability exception) 는 아직 Valen에 구현되지 않았음
-
객체를 변경하더라도 내부 데이터의 존재와 타입이 유지된다면 그 참조를 무효화하지 않는 규칙임
my_vec.push(42)가 mut(self)를 선언하더라도 my_vec.size는 계속 정수 필드로 존재하므로 그 참조를 유지할 수 있음
-
기본 타입, 구조체, 고정 크기 배열과 그 안의 타입 안정적인 데이터에 대한 참조는 유지함
-
열거형의 내용이나 포인터가 가리키는 대상처럼 타입 안정성이 없는 경계를 통과하는 참조는 무효화 대상임
-
예를 들어 Level 안에 Terrain과 두 개의 HashMap이 있을 때, 엔티티를 추가하면서 맵을 변경해도 level.terrain 참조는 유효하게 유지할 수 있음