1P by GN⁺ | ★ favorite | 댓글 1개
  • Ante는 참조 카운팅의 유연성과 빌림 검사의 안전성을 함께 쓰면서, Rust식 런타임 패닉이나 Swift식 독점 접근 검사 오버헤드를 피하려는 시스템 언어 설계임
  • 핵심 장치는 shape-stability와 temporary uniq conversion으로, 참조 카운팅된 값의 필드에는 안전하게 가변 빌림을 만들고 유니언 내부 값은 제한된 범위에서만 uniq로 다룸
  • Rust의 Rc<RefCell<T>>는 잘못 사용하면 런타임에 패닉이 날 수 있고, Swift의 borrowing system은 런타임 독점 접근 검사를 포함하지만, Ante는 일부 사례를 컴파일타임 규칙으로 처리하려 함
  • 아직 일부만 구현된 work-in-progress 설계이며, 타입을 재귀적으로 분석해 특정 객체에 도달 가능한지를 판단해야 하므로 필드 추가가 breaking API change가 될 수 있음
  • 이 접근은 shared mutable borrowing이 항상 불가능하다는 전제를 약화시키며, Vale, group borrowing, Rust GhostCell 같은 기법과 함께 메모리 안전성 설계의 예외 영역을 넓히고 있음

Ante가 목표로 하는 결합

  • Ante는 메모리 안전성스레드 안전성을 갖춘 더 단순한 Rust를 목표로 하는 시스템 프로그래밍 언어임
  • 기본 모델은 단일 소유권과 빌림 검사이며, 값은 스택이나 포함하는 구조체·배열 안에 인라인으로 놓임
  • 단순성을 우선하고 싶을 때는 타입에 shared 키워드를 붙여 참조 카운팅을 선택할 수 있음
  • shared type Color, shared type RbTree t를 사용한 red-black tree balance 함수는 Python 예시만큼 짧고 C++·Rust 예시보다 작음
  • 핵심 관심사는 참조 카운팅된 데이터를 가변으로 빌릴 때 Rust의 borrow_mut() 패닉 위험이나 Swift의 런타임 독점 접근 검사 없이 처리하는 방법임
  • Ante는 아직 work-in-progress 상태이며, 일부는 구현됐고 일부는 이론적이며 설계도 변하는 중임

shape-stability와 여러 가변 참조

  • Ante의 shape-stability는 “stable shape를 가진 대상에 대한 참조는 다른 곳에서 어떤 변경이 일어나도 항상 유효하다”는 개념임
  • 이 개념 덕분에 같은 구조체에 대해 여러 개의 가변 빌림 참조를 동시에 가질 수 있음
  • heal (healer: mut Entity) (target: mut Entity) 예시에서는 같은 Entity를 두 인자로 넘겨 자기 자신을 치유하는 self_heal 호출이 가능함
    • healertarget이 같은 Entity를 가리켜도 이 코드에서는 Entity를 파괴할 수 없으므로 두 참조가 계속 유효함
  • 구조체 자체와 그 필드, 필드의 필드에 대한 가변 참조도 동시에 허용될 수 있음
    • ship: mut Spaceshipengine_alias: mut Engine = ship.engine를 동시에 사용해도, 함수 실행 중 ship과 그 안의 engine이 파괴되지 않는다고 판단함
  • Rust와 Swift에서는 같은 데이터에 여러 &mut 참조가 동시에 가리키는 형태를 허용하지 않음

참조 카운팅된 값의 필드 가변 빌림

  • Ante에서 타입 정의 앞에 shared를 붙이면 해당 타입은 자동으로 참조 카운팅
  • shared mut type Spaceship 예시에서는 launchRc에 해당하는 Spaceship을 보유하면서 mut ship.engineset_fuel에 넘김
  • launch가 포함 객체인 Spaceship을 유지하므로, 그 필드인 engine도 살아 있다고 판단할 수 있음
  • 일반 규칙은 shared mut 타입의 필드에 대해 항상 mut 빌림 참조를 만들 수 있다는 것임
    • 단, 그 필드 안쪽에 들어 있는 모든 대상에 대해 항상 가변 빌림을 만들 수 있는 것은 아니며, 별도 규칙이 필요함
  • 이후 예시는 설탕 문법인 shared mut type Spaceship 대신 더 명시적인 Rc Spaceship 표기를 사용함
    • shared mut type Spaceshiptype Spaceship이 되고, var ship: Spaceshipvar ship: Rc Spaceship이 됨

유니언이 안전성 문제를 만드는 지점

  • 유니언은 내용을 인라인으로 보관해 포인터 추적과 캐시 미스를 줄일 수 있어 속도에 유리함
    • C의 union Enginestruct Spaceship 안에 들어가면 StringTheoryEngineImpulseEngineSpaceship 메모리 안에 위치함
    • Java처럼 인터페이스와 포인터를 사용하는 방식과 대비됨
  • 문제는 메모리 안전 언어에서 유니언을 안전하게 지원하기 어렵다는 점임
  • EngineStringTheoryEngine(str: String) 또는 ImpulseEngine(fuel: I32)인 예시에서는 shipother_ship이 같은 Spaceship을 가리킬 때 세그폴트가 날 수 있음
    • match uniq ship.engine으로 문자열 내부 참조를 잡은 뒤
    • other_ship.engine := ImpulseEngine 0x42로 같은 엔진을 다른 변형으로 바꾸고
    • 이어서 기존 str을 수정하면 컨테이너가 파괴된 뒤 내부를 사용하는 문제가 생김
  • 따라서 Ante는 가변 빌림 참조가 유니언을 가리킬 때 그 변형 중 하나에 대한 가변 빌림 참조를 만들 수 없도록 해야 함
  • 이는 구조체 규칙과 반대임
    • 구조체에 대한 mut 참조가 있으면 필드에 대한 mut 참조를 만들 수 있음
    • 유니언에 대한 mut 참조가 있으면 변형 내부에 대한 mut 참조를 만들 수 없음

uniq와 temporary uniq conversion

  • uniqexclusive mutable reference, 즉 독점 가변 참조를 뜻함
  • 어떤 변수가 uniq Spaceship을 담고 있다면, 그 Spaceship에 대한 유일하게 사용 가능한 참조임
    • Rust의 &mut Spaceship과 비슷한 개념임
  • 유니언 내부를 안전하게 다루기 위해 Ante는 temporary uniq conversion을 사용함
  • 핵심 규칙은 특정 범위에서 다른 별칭 가능 참조를 사용하지 않는다면 임시로 uniq 참조를 얻을 수 있다는 것임
    • match uniq ship.engine 구간에서는 ship.engine에 대해 uniq처럼 접근함
    • 이 구간 동안 컴파일러는 Spaceship을 간접적으로 포함할 수 있는 다른 기존 변수를 사용하지 못하게 함
  • Rust는 “다른 참조가 어딘가에 있을 수 있다”는 이유로 uniq 존재 자체를 막는 반면, Ante는 해당 범위에서 그 참조들을 사용하지 않는 조건으로 uniq를 허용함
  • 이때 uniq Spaceship은 실제로 전역적으로 유일한 참조가 아니라, 해당 범위 안에서 유일하게 사용 가능한 참조임
    • C의 restrict 포인터와 유사한 뉘앙스를 가짐

허용되는 접근과 거부되는 접근

  • match uniq ship.engine 범위 안에서 other_ship: Rc Spaceship에 접근하면 컴파일 오류가 나야 함
    • other_ship.engineship.engine과 alias일 수 있고
    • ship.engine을 사용하는 동안 other_ship.engine 변경이 drop을 유발할 수 있기 때문임
  • HasAShip처럼 Rc Spaceship을 필드로 가진 다른 구조체도 같은 이유로 거부됨
    • other.ship.engine도 간접적으로 같은 Spaceship에 도달할 수 있음
  • 반대로 new_fuel: I32 같은 정수는 사용할 수 있음
    • I32Spaceship에 대한 참조를 포함할 수 없기 때문임
  • Spaceship 자체가 follow_ship: Rc Spaceship 같은 필드를 포함하면 거부됨
    • 그 경우 uniq Spaceship도 자기 내부 경로를 통해 다시 도달 가능해지므로, 일반적으로 재귀 타입에는 mut -> uniq 변환을 할 수 없음

함수 호출과 반환에서의 제약

  • 함수 호출에서도 mut -> uniq 변환이 일어날 수 있음
  • foo (var ship: Rc Spaceship) (new_res: Resonator)maybe_use_resonator ship new_res를 호출할 때, 호출 지점에서 shipuniq Spaceship으로 변환됨
    • 컴파일러는 다른 인자가 Spaceship 참조를 포함할 수 있는지만 확인하면 됨
    • 예시의 Resonator는 그런 참조를 포함하지 않으므로 허용됨
  • 반환에서는 변환된 uniq 참조를 일반 uniq로 돌려줄 수 없음
    • 반환 후에는 “범위 안에서 alias 가능 변수를 사용하지 않는다”는 컴파일러 검사가 적용되지 않기 때문임
  • 대신 반환 타입을 local uniq Foo로 지정할 수 있음
    • 내부적으로 mut ref에서 uniq ref로 변환할 때 실제로는 항상 local uniq가 만들어짐
    • 대부분의 경우 일반 uniq처럼 사용할 수 있지만, 반환할 때는 명시가 필요함

설계상의 비용과 대안

  • Ante는 Rc Spaceship 같은 참조 카운팅 참조를 런타임 오류 없이 임시 uniq Spaceship으로 바꿀 수 있음
  • 단점은 컴파일러가 “Engine에서 Spaceship에 도달할 수 있는가” 같은 질문에 답하기 위해 타입을 재귀적으로 살펴봐야 한다는 점임
  • 이런 분석은 취약할 수 있음
    • 구조체에 필드를 추가하는 일이 breaking API change가 될 수 있음
  • Ante의 제작자인 Jake는 이 보장을 유지하는 더 나은 방법을 찾고 있음
    • group borrowingFlix references처럼 각 공유 가변 타입에 익명 고유 브랜드 타입을 붙이는 방식
    • 공유 타입을 변경할 때 Mutates 'a 같은 effect를 추가해 타입 분석을 제거하는 방식
    • 두 참조가 다른 객체를 가리키는지 사용자가 런타임에 검사하거나, safe API로 감싼 unsafe 검사를 제공하는 방식
    • 컴파일러가 Rc 내부에 간접 저장되지 않아 alias될 수 없는 값을 추적하는 방식
  • Pony의 iso permission과 비슷한 아이디어, 또는 구조체 내부를 보되 바깥을 가리키는 참조는 사용하지 못하게 하는 임시 권한도 가능성으로 남아 있음
  • 어려운 부분은 이런 유연성을 유지하면서 Ante의 목표인 사용성, 가독성, 단순성을 지키는 것임

더 넓은 메모리 안전성 흐름

  • shared mutable borrowing은 예전에는 불가능하다고 여겨졌고, Rust도 그런 믿음 위에 설계됐다는 관점을 깔고 있음
  • 여러 예외가 누적되고 있음
    • Ante는 local uniqueness 규칙을 통해 shared-mutable 데이터에서 uniq 빌림 참조를 얻을 수 있음
    • Vale은 pure function을 통해 shared-mutable 데이터에서 불변 빌림 참조를 얻을 수 있음
    • group borrowing은 shape-stable이 아니어도 shared-mutable 빌림 참조를 만들 수 있음
    • Rust의 GhostCell은 객체 그래프가 서로 자유롭게 가리킬 수 있게 하지만, 특정 시점에는 그중 하나에 대한 가변 참조 하나만 가질 수 있음
  • 이 흐름은 메모리 안전성 설계에서 shared mutable borrowing을 다루는 더 일반적인 원리가 있을 가능성을 암시함

Rust Cell과의 비교

  • Rust 사용자는 구조체 필드에 Cell을 넣는 방식과 Ante 접근의 차이를 물을 수 있음
  • Ante 예시에서는 Rc Spaceship에서 status: String에 대한 mut String 참조를 얻어 " (refueling)"을 직접 붙일 수 있음
  • Rust의 Cell<String> 방식에서는 Rc<Spaceship>에서 &mut String을 얻을 수 없음
    • 대신 status_ref.replace(String::new())로 임시 기본값을 넣고
    • 꺼낸 String을 수정한 뒤
    • 마지막에 다시 replace(status)로 되돌려야 함
  • 이 방식에는 몇 가지 단점이 있음
    • "" 같은 기본 인스턴스를 만들어야 함
    • 마지막 replace 호출을 잊을 위험이 있음
    • 값이 교체된 상태에서 누군가 status를 읽을 위험이 있음
  • Ante는 임시로 status 문자열에 대한 참조를 얻도록 하고, 그동안 다른 코드가 접근하지 못하게 컴파일러가 강제함

댓글과 토론

Lobste.rs 의견들
  • 공유된 가변 차용”이 불가능하다고 여겼던 것은 단순히 Rust가 목표를 달성하려고 감수한 희생이 아니라, Rust의 핵심 목표 자체에 가까움
    공유 가변 상태는 코드에 대한 국소적 추론을 어렵게 만들기 때문임
    "References are like jumps" by withoutboats가 이 점을 잘 다룸. 별칭이 있는 상태를 우연히 변경하지 못하게 막는 것이 올바르게 동작하는 시스템을 쉽게 만들기 위한 핵심이고, Rust의 수명 규칙은 단순히 가비지 컬렉션을 피하려는 장치가 아니라 가변 상태와 별칭 상태를 함께 허용하는 언어에서 추론 가능성을 확보하는 더 깊은 구조라는 주장임

    • 예전에 저자가 Mojo borrow checker를 다뤘을 때도 같은 생각이 들었음. Rust의 차용 검사기는 단일 스레드 프로그램에서도 값 의미론을 유지해 줌
  • 꽤 괜찮아 보임
    이해한 게 맞다면, 공유 참조에서 가변 참조로 넘어가는 마법은 스레드 간에 공유되지 않는 타입에 한정되기 때문에 가능하고, Rc의 유일성은 같은 타입의 모든 객체가 같은 수명으로 차용된 것처럼 취급해서 보장하는 듯함
    명시적 문법과 자연스러운 문법 중 무엇이 좋은지는 취향 문제일 수 있지만, 컴파일러가 Cell에 대해 더 많이 알면 그에 대한 가변 참조를 더 유연하게 허용할 수 있음을 보여줌
    그리고 Rust에서 mut가 가변이 아니라 배타적/유일함을 뜻하는 것처럼 쓰이는 혼란스러운 용어도 피함

    • 스레드 간에서는 어떻게 되는지 궁금했음. “uniq 승격이 잠금 획득을 의미하나?” 같은 의문이었는데, 비교 대상이 Arc가 아니라 Rc 라는 뜻으로 이해됨
    • mut가 배타적/유일함을 뜻한다는 부분을 좀 더 설명해 줄 수 있음?
  • 끝부분에서 암시한 통합 원리가 무엇일지 짐작 가는 사람이 있는지 궁금함

  • antelang.org 블로그 글에 대한 이전 논의들도 참고할 만함

  • 이게 어떻게 동작하는지 잘 모르겠음. “객체에 대한 가변 포인터가 있으면 그 객체의 슬라이스에 대한 변경 참조를 얻을 수 있다”는 뜻처럼 보임
    그런데 그렇다면 예를 들어 mutref someobjext = …, mutref subfield = someobjext.a.b, someobjext.a = somethingelse 같은 식이 가능해 보이고, 그러면 subfield가 무효가 되거나 값이 바뀌면서 깨질 수 있음
    글에는 설명, 다른 언어와의 비교, 코드 예시는 많았지만, 정작 이 동작의 단계별 의미론을 기본적으로 정리한 부분을 찾기 어려웠음