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:
+79
-40
@@ -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)
|
||||
}
|
||||
|
||||
+10
-7
@@ -12,7 +12,7 @@
|
||||
| 02_higher_order_effects | effect 변수, 구문 수준 제한, 명시적 인스턴스화 |
|
||||
| 03_scope_concurrency | TaskScope, 이름 있는 scope, 중첩 시 수명 표현 |
|
||||
| 04_enum_match_interface | enum 정의 본문이 interface surface에 들어가는 경로 |
|
||||
| 05_move_errors | **에러가 나야 하는** 코드 — 진단 하나씩 |
|
||||
| 05_move_errors | **move/affinity 검사기가** 거부해야 하는 코드 (자원을 직접 정의) |
|
||||
| 06_affine_closure | callable affinity (fn vs affine fn), own과의 직교성 |
|
||||
| 07_module_interface | interface artifact가 담아야 할 것 전부 |
|
||||
| 08_syntax_errors | **파서가** 거부해야 하는 코드 |
|
||||
@@ -21,13 +21,16 @@
|
||||
|
||||
05, 08, 09, 10은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가
|
||||
기대 진단이며, 넷의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가,
|
||||
10은 effect 검사기가, 05는 아직 없는 move/affinity 검사가** 거부해야 한다.
|
||||
단계별로 파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에
|
||||
영영 도달하지 못하기 때문이다.
|
||||
10은 effect 검사기가, 05는 move/affinity 검사가** 거부해야 한다. 단계별로
|
||||
파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영
|
||||
도달하지 못하기 때문이다.
|
||||
|
||||
09와 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
|
||||
TUnknown으로 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다.
|
||||
10은 capability를 직접 정의해야 메서드의 effect가 알려지므로 특히 그렇다.
|
||||
05, 09, 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
|
||||
빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일들이다. 10은
|
||||
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
|
||||
capability라 자원 타입을 정의해야 affine임이 유도된다.
|
||||
|
||||
01~04, 06, 07은 `cool check`를 통과한다 (exit 0).
|
||||
|
||||
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user