move: move/affinity 검사 — v0 fast path 완성

보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로
닫는다. 검사는 전부 함수 로컬 데이터플로우이고 전역 분석이 없다.

affinity의 뿌리는 capability다. 필드로 가진 타입은 전이적으로 affine이며
고정점까지 돌려 상호 재귀 타입도 유도한다. 이 전이가 없으면 wrapper 하나를
복사해 capability가 사실상 복제되므로 정리가 깨진다. copyable 선언과 affine
필드의 공존은 오류다.

구현한 규칙:
- affine 값은 소유 자리로 갈 때 move된다(own 파라미터, 반환, struct 저장,
  컨테이너 삽입, let 바인딩, by-move capture). moved 이후 사용은 오류이고
  진단이 어디서 소비됐는지를 말한다
- 분기 병합은 보수적 합집합. 한 분기에서라도 moved면 병합 이후 moved
- 빌린 값은 탈출하지 못한다: 반환, struct 저장, 소유 자리로 넘기기 전부 거부
- use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라 소유 자리로
  갈 수 없다. 별도의 nonescaping 개념 없이 use 규칙 하나로 닫힌다
- callable affinity: affine 값을 capture한 클로저는 affine fn이며 fn 자리에
  갈 수 없다

자율 결정 둘:
- 클로저는 mut 바인딩을 capture할 수 없다. spawn만 막는 특수 규칙 대신
  일반 규칙으로 뒀다 — v0에 참조가 없으므로 별칭도 조용한 복사도 만들 수
  없고, spawn 제한은 이 규칙의 특수 사례가 된다
- v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다.
  affine 필드만 꺼내려면 부분 move 상태 추적이 필요한데 v0가 살 복잡도가 아니다

05를 자족적으로 다시 썼다. affinity의 뿌리가 capability라 자원 타입을 모듈
안에서 정의해야 검사기가 affine임을 유도할 수 있다. 외부 타입은 affine임을
증명할 수 없으므로 copyable로 본다.

이로써 fast path(L0 parse / L1 type·effect·capability·ownership)가 완성됐다.
cool check가 처음으로 성공을 선언한다 — 01~04, 06, 07이 exit 0으로 통과한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 03:01:05 +09:00
co-authored by Claude Opus 5
parent 79e4ed4190
commit 2e67b74376
6 changed files with 722 additions and 79 deletions
+79 -40
View File
@@ -1,26 +1,38 @@
// 05. 컴파일 에러가 나야 하는 코드
// 05. move / affinity 검사기가 거부해야 하는 코드
//
// 각 함수는 주석에 적힌 진단 하나를 정확히 내야 한다.
// 체커가 생기면 그대로 테스트 케이스가 된다.
// 09, 10과 같은 이유로 외부 타입이 하나도 없다. affinity의 뿌리는 capability라
// 자원 타입을 이 파일에서 정의해야 검사기가 affine임을 알 수 있다.
// 외부 타입은 affine임을 증명할 수 없으므로 copyable로 취급된다.
// close는 파일을 소비한다: own 유표기
pub capability File {
fn size() -> Int
}
pub capability Gateway {
fn refund(id: Int) effects {Gateway.refund}
}
pub capability Registry {
fn add(h: fn()) effects {Registry.add}
}
// 파일을 소비하는 함수: own 유표기
pub fn close(own f: File) effects {File.close}
// [E-move-after-move] affine 값의 이중 소비
pub fn double_close(own f: File) effects {File.close} {
// 빌리기만 하는 함수: 무표기
pub fn size_of(f: File) -> Int {
f.size()
}
// --- 통과해야 하는 것 ---
pub fn use_then_close(own f: File) effects {File.close} -> Int {
let n = size_of(f)
close(f)
close(f) // ERROR: f는 이미 move됨 (앞줄에서 소비)
n
}
// [E-move-join] 분기 병합은 보수적 합집합
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
}
close(f) // ERROR: f는 이 분기에서 move됨 (조건부 소비)
}
// 정당한 형태 — 양쪽 분기에서 소비하면 통과해야 한다.
// 양쪽 분기에서 소비하면 통과한다
pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
@@ -29,48 +41,75 @@ pub fn both_branches_close(own f: File, c: Bool) effects {File.close} {
}
}
// 빌린 값을 다른 빌림 자리로 넘기는 것은 복제가 아니다
pub fn borrow_twice(f: File) -> Int {
size_of(f) + size_of(f)
}
// affine 값을 capture한 클로저는 affine fn이다
pub fn deferred_close(own f: File) -> affine fn() effects {File.close} {
fn() { close(f) }
}
// --- 여기서부터 전부 오류다 ---
// [E-move-after-move] affine 값의 이중 소비
pub fn double_close(own f: File) effects {File.close} {
close(f)
close(f)
}
// [E-move-join] 분기 병합은 보수적 합집합
pub fn conditional_close(own f: File, c: Bool) effects {File.close} {
if c {
close(f)
}
close(f)
}
// [E-use-escape] 빌린 값의 반환
pub fn leak_capability(pay: PaymentGateway) -> PaymentGateway {
pay // ERROR: 빌린 값은 반환할 수 없음 (own이 아니다)
pub fn leak_capability(pay: Gateway) -> Gateway {
pay
}
// [E-use-escape] 빌린 값의 저장
pub struct Holder {
pay: PaymentGateway,
pay: Gateway,
}
pub fn store_capability(pay: PaymentGateway) -> Holder {
Holder { pay: pay } // ERROR: 빌린 값은 struct에 저장할 수 없음
pub fn store_capability(pay: Gateway) -> Holder {
Holder { pay: pay }
}
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 전달
pub fn register(own handler: fn()) effects {Registry.add}
// [E-use-escape] 빌린 값을 다른 함수에 소유로 넘긴다
pub fn give_away(f: File) effects {File.close} {
close(f)
}
pub fn escape_via_closure(pay: PaymentGateway) effects {Registry.add} {
register(fn() { pay.refund(OrderId(1)) })
// ERROR: pay를 capture한 클로저는 빌린 값이며 own 자리에 전달할 수 없음
// [E-use-escape] 빌린 값을 capture한 클로저를 own 자리에 넘긴다
pub fn register(own h: fn() effects {Gateway.refund}) effects {Registry.add}
pub fn escape_via_closure(pay: Gateway) effects {Registry.add} {
register(fn() { pay.refund(1) })
}
// [E-affinity-transitive] affine 필드를 가진 타입을 copyable로 선언
pub copyable struct Box {
f: File, // ERROR: affine 필드(File)와 copyable 선언은 공존할 수 없음
f: File,
}
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 대입
// [E-callable-affinity] affine 값을 capture한 클로저를 fn 위치에 반환
pub fn misuse_affine_closure(own f: File) -> fn() effects {File.close} {
fn() { close(f) }
// ERROR: f를 capture했으므로 타입은 affine fn()이며 fn() 위치에 대입할 수 없음
}
// [E-spawn-capture] spawn 클로저 mutable capture
pub fn spawn_mutable(sc: TaskScope, mut counter: Int) effects {TaskScope.spawn} {
scope s = sc {
sc.spawn(fn() { counter = counter + 1 })
// ERROR: spawn 클로저는 mutable 참조를 capture할 수 없음
}
}
// [E-effect-undeclared] 선언되지 않은 effect
pub fn silent_write(log: Logger) {
log.write("hi") // ERROR: effect Logger.write가 시그니처에 선언되지 않음
// [E-closure-mut-capture] 클로저 mut 바인딩을 capture할 수 없다
pub fn capture_mut(own f: File, pay: Gateway)
effects {File.close, Registry.add, Gateway.refund} {
let mut counter = 0
register(fn() {
counter = counter + 1
pay.refund(counter)
})
close(f)
}