diff --git a/docs/thesis.md b/docs/thesis.md index f80ce1a..96ee93d 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -444,6 +444,15 @@ interface hash에 들어가지 않는다 — 테스트를 고쳤다고 downstrea ※ effect 있는 코드는 테스트할 수 없다. 가짜 capability를 만드는 수단이 없기 때문이다. 실제로 불편해진 뒤에 판단한다. +■ 버려지는 값 +꼬리가 아닌 자리의 식이 값을 남기면 오류다. 남긴 값은 버려지는데, 그 값이 +Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다. +일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다. +※ 개밥 먹기 2에서 발견했다. 그전까지 "이 언어에는 실패를 버릴 방법이 없다"고 + 문서와 투어에 적혀 있었는데 틀린 말이었다 — List.each에 Result 반환 클로저를 + 못 넘긴다는 좁은 사실을 언어 전체의 성질로 일반화한 것이었다. + 부산물로 정리 경로의 관용구가 생겼다: let _ = fs.close(f). + ■ lint (오류다, 경고가 아니다) - 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다. 증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다. @@ -472,7 +481,21 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌 것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다. 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. -■ 개밥 먹기 (2026-08) +■ 개밥 먹기 2 — dogfoods/ (2026-08) +프로그램을 쓰되 돌리지 않는다. std를 선언만 두고 그것에 대고 실제 유스케이스를 +쓴다. std가 원래 선언-전용이므로 실행만 빼고 전부 진짜로 검사된다 — 종이 +스케치가 아니라 컴파일러가 검증한 설계다. +합격 기준이 둘이다: check exit 0 + 모듈이 실제로 해소될 것. 후자가 없으면 +전자가 공허하다 — import가 조용히 해소되지 않으면 이름이 전부 불투명해져 +검사기가 무엇이든 통과시킨다. 첫 시도에서 실제로 그 일이 일어났다. +2번(Atomic File Updater) 결과: ?를 자원과 함께 쓸 수 없다. 조기 반환이 정리를 +건너뛰는데 검사기는 통과시킨다(v0 정책: 오용 금지, 누수 허용). 제대로 정리하면 +18줄이 58줄이 되고 5단 중첩 match가 된다. resource/with가 필요한 이유가 여기 +숫자로 있다. +열린 것: 함수 타입에 own이 없어 고차 경계에서 소유권 검사가 뚫린다. +전문은 dogfoods/FINDINGS.md. + +■ 개밥 먹기 1 — samples/app (2026-08) samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고 쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다. 결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect diff --git a/dogfoods/FINDINGS.md b/dogfoods/FINDINGS.md new file mode 100644 index 0000000..2819166 --- /dev/null +++ b/dogfoods/FINDINGS.md @@ -0,0 +1,150 @@ +# 개밥 먹기 발견 기록 + +`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. 값 있는 식을 문으로 버릴 수 있었다 — 고침 + +```cool +fs.remove(path) // Result가 조용히 사라진다 +1 +``` + +이것이 통과했다. 즉 **`Result`를 버리는 방법이 있었다.** 개밥 먹기 1차 +보고서와 언어 투어에서 내가 "이 언어에는 실패를 버릴 방법이 없다"고 적었는데 +틀렸다 — `List.each`에 Result 반환 클로저를 못 넘긴다는 좁은 사실을 언어 전체의 +성질로 일반화했다. + +이제 꼬리가 아닌 자리의 식이 값을 남기면 오류다: + +``` +이 식이 남기는 Result[Unit, IoError]이(가) 버려집니다 +(일부러 버리려면 let _ = 로 적으십시오) +``` + +부산물이 좋다. 정리 경로에서 오류를 **일부러** 무시하는 관용구가 생겼고, +버린다는 사실이 코드에 보인다: + +```cool +let _ = fs.close(f) // 정리 중의 실패는 삼킨다 — 원래 오류가 더 중요하다 +discard(fs, tmp, e) +``` + +### D3. `?`를 자원과 함께 쓸 수 없다 — 이번 개밥 먹기의 본체 + +`naive.cool` 18줄은 이렇게 생겼고 **통과한다**: + +```cool +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`의 메서드로 두고 핸들을 인자로 받는다: + +```cool +fn close(own f: WriteFile) effects {Fs.close} -> Result[Unit, IoError] +``` + +`f.close()` 대신 `fs.close(f)`가 된다. 읽기에 나쁘지 않고 오히려 권한 +(`Fs`)이 필요하다는 게 보인다. **지금은 우회로 충분해 보인다.** + +### D5. 함수 타입에 `own`이 없다 — 열림, 그리고 이건 구멍이다 + +```cool +pub fn apply(f: fn(own Handle) -> Handle, own h: Handle) -> Handle + ^^^ 타입이(가) 필요합니다 — own 발견 +``` + +문법이 `fn_type = "fn" "(" type_list ")"`라 `own`이 못 들어간다. 그래서 +**소유권을 가져가는 클로저를 타입으로 표현할 수 없다.** + +결과로 이것이 통과한다: + +```cool +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`가 닫으려는 자리다. diff --git a/dogfoods/README.md b/dogfoods/README.md new file mode 100644 index 0000000..52555f3 --- /dev/null +++ b/dogfoods/README.md @@ -0,0 +1,66 @@ +# dogfoods — 실제 유스케이스로 언어를 압박한다 + +`samples/`가 검사기를 시험한다면 여기는 **언어가 실제 문제를 표현할 수 있는지**를 +시험한다. 성격이 다르므로 폴더를 나눈다. + +## 방법 + +프로그램을 **쓰되 돌리지 않는다.** `std-draft/`에 런타임이 구현한다고 가정한 +선언을 두고, 그것에 대고 실제 프로그램을 쓴다. + +"가상"이 아니다. `std/*.cool`이 원래 본문 없는 선언이므로 **실행만 빼고 전부 +진짜로 검사된다**: + +``` +lexer ✓ parser ✓ 이름 해소 ✓ 타입 ✓ effect ✓ +capability ✓ move/affinity ✓ 소진성 ✓ interface hash ✓ +run ✗ +``` + +종이 스케치가 아니라 컴파일러가 검증한 설계다. + +## 합격 기준 + +``` +① coolc check exit 0 +② 모듈이 실제로 해소될 것 +``` + +②가 없으면 ①이 공허하다. import가 조용히 해소되지 않으면 그 모듈의 이름이 +전부 불투명해지고, 검사기는 "모르는 것을 틀렸다고 말하지 않는다"는 원칙에 따라 +무엇이든 통과시킨다. **실제로 첫 시도에서 이 일이 일어났다** (D1 참고). + +## 규율 + +`std-draft`의 선언은 독립적으로 정당화되어야 한다. + +- 지금 런타임이 가진 권한으로 구현 가능할 것 +- effect를 전부 선언할 것 +- **실패 양상을 선언하는 자리에서 정할 것** — 무엇이 `Result`이고 무엇이 + `crash`인지 +- 없는 언어 기능에 기대지 말 것. 필요하다는 것이 드러나면 **그것이 발견이지 + 지름길이 아니다** + +프로그램이 예뻐 보이도록 API를 발명하면 아무것도 배우지 못한다. + +## 한계 + +이 기준이 증명하는 것은 **표현 가능성**이지 의미론의 정확성이 아니다. +`check`가 통과한다고 그 프로그램이 옳게 도는 것은 아니다 — 돌려본 적이 없다. + +## 세트 + +| | | 압박하는 곳 | 상태 | +|---|---|---|---| +| 1 | Mini Shell | 소유권 / OS 핸들 | 대기 | +| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** | +| 3 | Process Supervisor | 실패 위상 | 대기 | +| 4 | Reverse Proxy | 취소, backpressure | 대기 | +| 5 | TCP Server | 규모의 API 모양 | 대기 | +| 6 | DB Pool | lease, 자원 정리 | 대기 | +| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 | + +순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는 +**동시성을 안 섞고 자원 모델만 시험할 수 있는 유일한 것**이기 때문이다. + +발견은 `FINDINGS.md`에 쌓고, 확정된 결정은 `docs/thesis.md`로 올린다. diff --git a/dogfoods/atomic-update/careful.cool b/dogfoods/atomic-update/careful.cool new file mode 100644 index 0000000..c728c6b --- /dev/null +++ b/dogfoods/atomic-update/careful.cool @@ -0,0 +1,58 @@ +// 제대로 정리하는 버전. +// +// naive.cool과 같은 일을 하지만 모든 실패 경로에서 핸들을 닫고 임시 파일을 +// 지운다. 두 파일의 차이가 이 개밥 먹기의 산출물이다. + +import "../std-draft/fs" as Fs +import "cool.dev/std/string" as String + +pub fn update(fs: Fs.Fs, path: String, contents: String) + effects {Fs.create, Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove} + -> Result[Unit, Fs.IoError] { + let tmp = String.concat(path, ".tmp") + match fs.create(tmp) { + Err(e) => Err(e), + Ok(f) => finish(fs, f, tmp, path, contents), + } +} + +// 핸들을 얻은 뒤. 여기서부터 모든 실패 경로가 f를 닫고 tmp를 지워야 한다. +// ? 를 쓸 수 없다 — 조기 반환이 정리를 건너뛰기 때문이다. +pub fn finish( + fs: Fs.Fs, + own f: Fs.WriteFile, + tmp: String, + path: String, + contents: String, +) effects {Fs.write, Fs.sync, Fs.close, Fs.rename, Fs.remove} + -> Result[Unit, Fs.IoError] { + match fs.write(f, contents) { + Err(e) => abandon(fs, f, tmp, e), + Ok(_) => match fs.sync(f) { + Err(e) => abandon(fs, f, tmp, e), + Ok(_) => match fs.close(f) { + Err(e) => discard(fs, tmp, e), + Ok(_) => match fs.rename(tmp, path) { + Err(e) => discard(fs, tmp, e), + Ok(_) => Ok(unit), + }, + }, + }, + } +} + +// 핸들을 아직 들고 있는 실패. 닫고 지운다. +// 정리 중의 실패는 일부러 버린다 — 원래 오류가 더 중요하다. +// let _ = 가 버린다는 사실을 코드에 보이게 한다. +pub fn abandon(fs: Fs.Fs, own f: Fs.WriteFile, tmp: String, e: Fs.IoError) + effects {Fs.close, Fs.remove} -> Result[Unit, Fs.IoError] { + let _ = fs.close(f) + discard(fs, tmp, e) +} + +// 핸들은 이미 없다. 임시 파일만 지운다. +pub fn discard(fs: Fs.Fs, tmp: String, e: Fs.IoError) + effects {Fs.remove} -> Result[Unit, Fs.IoError] { + let _ = fs.remove(tmp) + Err(e) +} diff --git a/dogfoods/atomic-update/naive.cool b/dogfoods/atomic-update/naive.cool new file mode 100644 index 0000000..f5de02c --- /dev/null +++ b/dogfoods/atomic-update/naive.cool @@ -0,0 +1,18 @@ +// 순진한 버전 — ?로 짧게 쓴 것. +// +// 이 파일의 목적은 통과하는 것이 아니라, 통과한다는 사실을 보여주는 것이다. + +import "../std-draft/fs" as Fs +import "cool.dev/std/string" as String + +pub fn update(fs: Fs.Fs, path: String, contents: String) + effects {Fs.create, Fs.write, Fs.sync, Fs.close, Fs.rename} + -> Result[Unit, Fs.IoError] { + let tmp = String.concat(path, ".tmp") + let f = fs.create(tmp)? + fs.write(f, contents)? + fs.sync(f)? + fs.close(f)? + fs.rename(tmp, path)? + Ok(unit) +} diff --git a/dogfoods/std-draft/fs.cool b/dogfoods/std-draft/fs.cool new file mode 100644 index 0000000..4f419fe --- /dev/null +++ b/dogfoods/std-draft/fs.cool @@ -0,0 +1,65 @@ +// 파일 시스템 — 개밥 먹기용 초안. +// +// 이 파일은 구현이 아니라 계약이다. 런타임이 구현한다고 가정하고, 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] +} diff --git a/lib/session.ml b/lib/session.ml index bba830c..6d7b153 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -65,9 +65,18 @@ let create ?(root = ".") ?std () = 패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런 import는 불투명하게 남는다 — 없다고 말하지 않는다. *) let is_package path = - match String.index_opt path '/' with - | Some i -> String.contains (String.sub path 0 i) '.' - | None -> String.contains path '.' + (* ./ 와 ../ 로 시작하면 상대 경로다. 첫 세그먼트에 점이 있다는 것만 보면 + ".."이 패키지로 오인된다 — 그러면 import가 조용히 해소되지 않고, 그 + 모듈의 이름이 전부 불투명해져 검사가 통째로 공허해진다. *) + let starts p = + String.length path >= String.length p + && String.sub path 0 (String.length p) = p + in + if starts "./" || starts "../" then false + else + match String.index_opt path '/' with + | Some i -> String.contains (String.sub path 0 i) '.' + | None -> String.contains path '.' let std_prefix = "cool.dev/std/" diff --git a/lib/typecheck.ml b/lib/typecheck.ml index b37a7aa..ceae0fe 100644 --- a/lib/typecheck.ml +++ b/lib/typecheck.ml @@ -43,6 +43,26 @@ type env = { let err env pos msg = env.errors <- { pos; msg } :: env.errors +let expr_pos (e : expr) : Token.pos = + match e with + | E_lit (_, p) + | E_ident (_, p) + | E_list (_, p) + | E_struct { pos = p; _ } + | E_if { pos = p; _ } + | E_match { pos = p; _ } + | E_scope { pos = p; _ } + | E_call { pos = p; _ } + | E_field { pos = p; _ } + | E_inst { pos = p; _ } + | E_try { pos = p; _ } + | E_unary { pos = p; _ } + | E_binary { pos = p; _ } + | E_crash { pos = p; _ } -> + p + | E_closure c -> c.cl_pos + | E_block b -> b.block_pos + let mismatch env pos expected got what = err env pos (Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got)) @@ -646,6 +666,17 @@ and infer_block env (b : block) : T.t = go b.stmts and check_stmt env = function + (* 꼬리가 아닌 자리의 식은 값을 남기면 안 된다. 남긴 값은 버려지는데, + 그 값이 Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다. + 일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다. *) + | S_expr e -> ( + let t = infer env e in + match T.resolve t with + | T.TUnit | T.TUnknown | T.TNever -> () + | other -> + err env (expr_pos e) + (Printf.sprintf "이 식이 남기는 %s이(가) 버려집니다 (일부러 버리려면 let _ = 로 적으십시오)" + (T.show other))) | S_let { pat; ty; value; pos; _ } -> let declared = Option.map (conv env []) ty in let got = @@ -668,7 +699,6 @@ and check_stmt env = function let p = infer env place in let v = infer env value in if not (T.unify p v) then mismatch env pos p v "대입" - | S_expr e -> ignore (infer env e) (* ------------------------------------------------------------------ *) (* 모듈 *) diff --git a/test/dune b/test/dune index 880e8d2..f91217a 100644 --- a/test/dune +++ b/test/dune @@ -5,6 +5,7 @@ (glob_files %{workspace_root}/samples/*.cool) (glob_files %{workspace_root}/std/*.cool) (glob_files %{workspace_root}/docs/*.ebnf) + (glob_files_rec %{workspace_root}/dogfoods/**) (glob_files %{workspace_root}/samples/app/*) (glob_files %{workspace_root}/samples/modules/*.cool) (glob_files %{workspace_root}/samples/run/*.cool))) diff --git a/test/test_coollang.ml b/test/test_coollang.ml index d126a1e..1c5d967 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -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 = [])