프로그램을 쓰되 돌리지 않는 방식을 시작한다. 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
6.1 KiB
개밥 먹기 발견 기록
dogfoods/를 쓰면서 나온 것들. 고친 것과 열어둔 것을 같이 적는다.
2. Atomic File Updater (2026-08-30)
목적: 동시성을 안 섞고 자원 모델만 시험한다. 임시 파일에 쓰고, 디스크에 내리고, 원자적으로 바꿔치기한다. 실패 경로가 넷이고 전부 정리가 필요하다.
산출물: std-draft/fs.cool 65줄, atomic-update/naive.cool 18줄,
atomic-update/careful.cool 58줄.
D1. 상대 경로 import가 패키지로 오인됐다 — 고침
import "../std-draft/fs"가 조용히 해소되지 않았다. is_package가 첫
세그먼트에 점이 있는지만 봤는데 ..이 걸렸다.
무서운 것은 버그 자체가 아니라 그 결과다. import가 해소되지 않으면 그
모듈의 이름이 전부 불투명해지고, "모르는 것을 틀렸다고 말하지 않는다"는 원칙에
따라 검사기가 무엇이든 통과시킨다. 첫 coolc check 결과가 exit 0이었는데
아무 뜻도 없었다. 없는 메서드를 불러도 통과했다.
고친 뒤 같은 코드가 정확히 잡힌다:
capability Fs.Fs에 this_does_not_exist 메서드가 없습니다
→ dogfoods/README.md의 합격 기준 ②가 여기서 나왔다.
D2. 값 있는 식을 문으로 버릴 수 있었다 — 고침
fs.remove(path) // Result가 조용히 사라진다
1
이것이 통과했다. 즉 Result를 버리는 방법이 있었다. 개밥 먹기 1차
보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데
틀렸다 — List.each에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의
성질로 일반화했다.
이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다:
이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다
(일부러 버리려면 let _ = 로 적으십시오)
부산물이 좋다. 정리 경로에서 오류를 일부러 무시하는 관용구가 생겼고, 버린다는 사실이 코드에 보인다:
let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다
discard(fs, tmp, e)
D3. ?를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체
naive.cool 18줄은 이렇게 생겼고 통과한다:
let f = fs.create(tmp)?
fs.write(f, contents)? // 실패하면 f가 안 닫히고 tmp가 남는다
fs.sync(f)?
fs.close(f)?
fs.rename(tmp, path)?
?가 조기 반환하므로 정리를 건너뛴다. v0가 "오용 금지, 누수 허용"이라
검사기는 아무 말도 하지 않는다.
제대로 정리하면 careful.cool 58줄이 되고 5단 중첩 match가 된다.
?를 한 번도 못 쓴다. 같은 일에 3.2배다.
이것이 resource / with 제안이 필요한 이유의 전부다. 그리고 그 제안이
암묵적 drop보다 나은 이유도 여기서 보인다 — 정리 지점이 닫는 중괄호로 눈에
보이고, Fs.close가 effects 절에 나타나 검사된다.
D4. capability 메서드가 수신자를 소비할 수 없다 — 열림
close가 핸들을 소비해야 하는데 own은 파라미터에만 붙는다. capability
메서드에는 수신자를 적는 자리가 없다.
우회했다 — 핸들의 메서드가 아니라 Fs의 메서드로 두고 핸들을 인자로 받는다:
fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError]
f.close() 대신 fs.close(f)가 된다. 읽기에 나쁘지 않고 오히려 권한
(Fs)이 필요하다는 게 보인다. 지금은 우회로 충분해 보인다.
D5. 함수 타입에 own이 없다 — 열림, 그리고 이건 구멍이다
pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle
^^^ 타입이(가) 필요합니다 — own 발견
문법이 fn_type = "fn" "(" type_list ")"라 own이 못 들어간다. 그래서
소유권을 가져가는 클로저를 타입으로 표현할 수 없다.
결과로 이것이 통과한다:
List.fold(xs, h, fn(acc, n) { step(acc, n) }) // step은 own을 받는다
fold의 시그니처는 fn(acc, a) -> acc이고 무표기는 빌림인데, 클로저
리터럴의 파라미터에는 소유권 표시가 없어 move 검사기가 소유한 값처럼
취급한다. 타입은 "빌린다"고 말하는데 리터럴은 "가져간다"처럼 행동한다.
기본 검사는 멀쩡하다 — 빌린 값 옮기기도 이중 소비도 정확히 잡는다. 고차 경계에서만 뚫린다. Atomic Updater는 선형 코드라 안 걸렸지만, Mini Shell(파이프라인을 따라 FD를 나름)과 DB Pool(lease를 fold로 다룸)은 정면으로 걸린다.
→ 그 둘을 쓰기 전에 결정해야 한다.
D6. 문자에 접근할 방법이 없다 — 열림
String에 split, trim, starts_with, contains뿐이다. 인덱싱도
chars도 없어 어휘 분석을 쓸 수 없다. Atomic Updater에는 파싱이 없어 안
걸렸지만 Mini Shell은 여기서 막힌다.
되돌리기 싼 std 문제다.
D7. effect-free 테스트로는 이 프로그램을 하나도 테스트할 수 없다 — 예상됨
update는 전부 effect다. 테스트는 capability를 받지 않으므로 부를 수 없다.
설계대로이고, 대가가 이제 실물로 보인다.
가짜 capability를 만들 수단이 없는 한 이 층은 테스트 밖에 있다.
※ 가짜 capability를 허용해도 보안 성질은 안 깨질 것으로 보인다 — 클로저로
만든 가짜 Fs는 진짜 권한을 갖지 않는다. 실제로 불편해진 뒤에 판단한다.
소유권 검사가 실제로 잡는 것 (확인)
| 두 번 닫기 | 잡힘 — f은(는) 이미 move되었습니다 (4:22에서 소비) |
| 닫은 뒤 쓰기 | 잡힘 — 같은 진단 |
| 빌린 핸들을 반환 | 잡힘 — 빌린 값이라 반환할 수 없습니다 |
| 소유한 핸들을 반환 | 통과 — 옳다. create가 그렇게 생겼다 |
| 조기 반환으로 누수 | 통과 — v0 정책("오용 금지, 누수 허용") |
마지막 줄이 D3이고, with가 닫으려는 자리다.