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
+24 -1
View File
@@ -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
+150
View File
@@ -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`가 닫으려는 자리다.
+66
View File
@@ -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`로 올린다.
+58
View File
@@ -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)
}
+18
View File
@@ -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)
}
+65
View File
@@ -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]
}
+9
View File
@@ -65,6 +65,15 @@ let create ?(root = ".") ?std () =
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
let is_package 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 '.'
+31 -1
View File
@@ -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)
(* ------------------------------------------------------------------ *)
(* 모듈 *)
+1
View File
@@ -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)))
+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 = [])