프로그램을 쓰되 돌리지 않는 방식을 시작한다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이 스케치가 아니라 컴파일러가 검증한 설계다. 합격 기준을 둘로 잡았다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면 전자가 공허한데, 그것을 첫 시도에서 겪었다. D1 — 상대 경로 import가 패키지로 오인됐다. is_package가 첫 세그먼트에 점이 있는지만 봐서 ".."이 걸렸다. 무서운 것은 버그가 아니라 결과였다: import가 해소되지 않으면 그 모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라 무엇이든 통과한다. 첫 check가 exit 0이었는데 없는 메서드를 불러도 통과하는 상태였다. D2 — 값 있는 식을 문으로 버릴 수 있었다. fs.remove(path)를 문으로 쓰면 Result가 조용히 사라졌다. 즉 실패를 버리는 방법이 있었고, 내가 개밥 먹기 1차 보고서와 투어에 "이 언어에는 실패를 버릴 방법이 없다"고 적은 것은 틀렸다 — List.each 하나의 좁은 사실을 언어 전체로 일반화했다. 이제 오류이고, 일부러 버리려면 let _ = 로 적는다. 부산물로 정리 경로의 관용구가 생겼다. D3(본체) — ?를 자원과 함께 쓸 수 없다. naive.cool 18줄은 조기 반환으로 핸들을 누수하는데 통과한다(v0 정책). 제대로 정리한 careful.cool은 58줄이고 5단 중첩 match이며 ?를 한 번도 못 쓴다. 3.2배다. resource/with가 필요한 이유가 여기 숫자로 있다. 열어둔 것: 함수 타입에 own이 없어 고차 경계에서 소유권이 뚫린다(D5). Mini Shell과 DB Pool이 정면으로 걸리므로 그 둘 전에 결정해야 한다. 문자 접근이 없어 어휘 분석을 못 쓴다(D6). 소유권 검사가 잡는 것은 확인했다: 두 번 닫기, 닫은 뒤 쓰기, 빌린 핸들 반환. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
66 lines
2.9 KiB
Plaintext
66 lines
2.9 KiB
Plaintext
// 파일 시스템 — 개밥 먹기용 초안.
|
|
//
|
|
// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, coollang이
|
|
// 이 일을 표현할 수 있는지만 본다. 실행은 안 되지만 타입·effect·capability·
|
|
// 소유권 검사는 전부 진짜로 돈다.
|
|
//
|
|
// 규율: 여기 적는 것은 지금 런타임이 가진 권한으로 구현 가능해야 하고,
|
|
// effect를 전부 선언해야 하며, 없는 언어 기능에 기대면 안 된다. 없는 기능이
|
|
// 필요하다는 게 드러나면 그것이 발견이지 지름길이 아니다.
|
|
|
|
// ------------------------------------------------------------------
|
|
// 실패는 무엇인가
|
|
//
|
|
// 여기서 Result와 crash의 선을 긋는다. 기준은 "호출자가 대처할 수 있는가"다.
|
|
// 대처할 수 있다 → Result
|
|
// 프로그램이 틀렸다 → crash
|
|
//
|
|
// 그래서 아래는 전부 Result다. 파일이 없는 것도, 권한이 없는 것도, 디스크가
|
|
// 가득 찬 것도 프로그램의 결함이 아니다 — 세상의 상태다.
|
|
// ------------------------------------------------------------------
|
|
|
|
pub enum IoError {
|
|
NotFound(String),
|
|
Denied(String),
|
|
Exists(String),
|
|
NoSpace(String),
|
|
// 나머지. 런타임이 분류하지 못한 것들
|
|
Other(String),
|
|
}
|
|
|
|
// 열린 쓰기 핸들.
|
|
//
|
|
// capability로 선언하는 이유가 둘이다.
|
|
// 1. 이것은 실제로 권한이다 — 이 파일에 쓸 수 있는 권한
|
|
// 2. capability는 affinity의 뿌리이므로 복제되지 않는다
|
|
// 메서드가 없다. 핸들로 할 수 있는 일은 Fs를 통해서 한다 — 아래 참고.
|
|
pub capability WriteFile {
|
|
}
|
|
|
|
// 파일 시스템에 손댈 권한.
|
|
//
|
|
// 핸들의 메서드가 아니라 Fs의 메서드로 둔 이유: close가 핸들을 소비해야
|
|
// 하는데, capability 메서드는 수신자를 소비할 방법이 없다. own은 파라미터에만
|
|
// 붙는다. 그래서 핸들을 인자로 받는 형태가 된다.
|
|
// ※ 발견 1: capability 메서드가 수신자를 소비할 수 없다.
|
|
pub capability Fs {
|
|
// 새로 만든다. 이미 있으면 자른다.
|
|
fn create(path: String) effects {Fs.create} -> Result[WriteFile, IoError]
|
|
|
|
// 핸들을 빌린다. 여러 번 쓸 수 있다.
|
|
fn write(f: WriteFile, s: String) effects {Fs.write} -> Result[Unit, IoError]
|
|
|
|
// 디스크까지 내려간다. 이게 없으면 rename이 원자적이어도 내용이 없을 수 있다.
|
|
fn sync(f: WriteFile) effects {Fs.sync} -> Result[Unit, IoError]
|
|
|
|
// 핸들을 소비한다. 두 번 닫을 수 없다 — own이 그것을 강제한다.
|
|
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError]
|
|
|
|
// 같은 파일 시스템 안에서 원자적이다.
|
|
fn rename(from: String, to: String) effects {Fs.rename} -> Result[Unit, IoError]
|
|
|
|
fn remove(path: String) effects {Fs.remove} -> Result[Unit, IoError]
|
|
|
|
fn read(path: String) effects {Fs.read} -> Result[String, IoError]
|
|
}
|