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:
+24
-1
@@ -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
|
||||
|
||||
@@ -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`가 닫으려는 자리다.
|
||||
@@ -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`로 올린다.
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 모듈 *)
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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 = [])
|
||||
|
||||
Reference in New Issue
Block a user