dogfoods: 2번 Atomic File Updater — ?를 자원과 함께 쓸 수 없다

프로그램을 쓰되 돌리지 않는 방식을 시작한다. 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
This commit is contained in:
2026-08-30 18:56:05 +09:00
co-authored by Claude Opus 5
parent 2307bafda2
commit 410363b230
10 changed files with 480 additions and 5 deletions
+55
View File
@@ -1653,3 +1653,58 @@ let () =
check "자식의 crash가 scope 밖으로 전파된다" false;
check "crash 전의 출력은 남는다" false;
check "scope 뒤는 실행되지 않는다" false
(* ------------------------------------------------------------------ *)
(* dogfoods — 언어가 실제 문제를 표현하는가 *)
(* *)
(* samples가 검사기를 시험한다면 여기는 언어를 시험한다. 통과가 회귀 *)
(* 테스트가 된다 — 규칙을 바꿨을 때 어느 유스케이스가 깨지는지 보인다. *)
(* *)
(* 기준이 둘인 이유: check만 보면 공허하다. import가 조용히 해소되지 *)
(* 않으면 그 모듈의 이름이 전부 불투명해져 무엇이든 통과한다. *)
(* 실제로 첫 시도에서 그 일이 일어났다 (FINDINGS D1). *)
(* ------------------------------------------------------------------ *)
let () =
let cases =
[
("../dogfoods/std-draft/fs.cool", []);
( "../dogfoods/atomic-update/naive.cool",
[ "../dogfoods/std-draft/fs.cool" ] );
( "../dogfoods/atomic-update/careful.cool",
[ "../dogfoods/std-draft/fs.cool" ] );
]
in
List.iter
(fun (path, deps) ->
let st = Session.create ~root:(Filename.dirname path) ~std:"../std" () in
Session.load st path;
let errs = Session.errors st in
List.iter
(fun (e : Session.error) ->
Printf.printf " (%s)\n" (Session.string_of_error e))
errs;
check (Filename.basename path ^ ": check 통과") (errs = []);
(* 의존 모듈이 실제로 로드됐는가 — 이게 없으면 위 줄이 공허하다.
경로 표기가 여러 가지라(../a/../b) 파일 이름으로 맞춘다 *)
let loaded =
Hashtbl.fold
(fun k _ acc -> Filename.basename k :: acc)
st.Session.modules []
in
List.iter
(fun d ->
check
(Filename.basename path ^ ": " ^ Filename.basename d
^ "이(가) 실제로 해소된다")
(List.mem (Filename.basename d) loaded))
deps)
cases
(* D3: 자원을 들고 ?를 쓰면 정리를 건너뛰는데 검사기는 통과시킨다.
v0 정책("오용 금지, 누수 허용")이고, with가 닫으려는 자리다.
이 테스트는 그 사실을 고정한다 — 나중에 정책이 바뀌면 여기가 깨진다. *)
let () =
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
Session.load st "../dogfoods/atomic-update/naive.cool";
check "누수는 아직 오류가 아니다 (v0 정책)" (Session.errors st = [])