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를 만드는 수단이
|
※ effect 있는 코드는 테스트할 수 없다. 가짜 capability를 만드는 수단이
|
||||||
없기 때문이다. 실제로 불편해진 뒤에 판단한다.
|
없기 때문이다. 실제로 불편해진 뒤에 판단한다.
|
||||||
|
|
||||||
|
■ 버려지는 값
|
||||||
|
꼬리가 아닌 자리의 식이 값을 남기면 오류다. 남긴 값은 버려지는데, 그 값이
|
||||||
|
Result면 실패가 조용히 사라진다 — 철학 1과 정면으로 부딪힌다.
|
||||||
|
일부러 버리려면 let _ = 로 적는다. 버린다는 사실이 코드에 보여야 한다.
|
||||||
|
※ 개밥 먹기 2에서 발견했다. 그전까지 "이 언어에는 실패를 버릴 방법이 없다"고
|
||||||
|
문서와 투어에 적혀 있었는데 틀린 말이었다 — List.each에 Result 반환 클로저를
|
||||||
|
못 넘긴다는 좁은 사실을 언어 전체의 성질로 일반화한 것이었다.
|
||||||
|
부산물로 정리 경로의 관용구가 생겼다: let _ = fs.close(f).
|
||||||
|
|
||||||
■ lint (오류다, 경고가 아니다)
|
■ lint (오류다, 경고가 아니다)
|
||||||
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||||
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
|
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
|
||||||
@@ -472,7 +481,21 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
|
|||||||
것이고, 이것이 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줄. 검사기를 시험하려고
|
samples/app — 설정 파서 + 리포트 도구, 2모듈 232줄. 검사기를 시험하려고
|
||||||
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
쓴 것이 아니라 일을 하려고 쓴 첫 프로그램이다.
|
||||||
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(capability 전달, effect
|
결과: 되돌리기 비싼 결정은 하나도 후회되지 않았고(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]
|
||||||
|
}
|
||||||
+12
-3
@@ -65,9 +65,18 @@ let create ?(root = ".") ?std () =
|
|||||||
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
|
패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
|
||||||
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
|
import는 불투명하게 남는다 — 없다고 말하지 않는다. *)
|
||||||
let is_package path =
|
let is_package path =
|
||||||
match String.index_opt path '/' with
|
(* ./ 와 ../ 로 시작하면 상대 경로다. 첫 세그먼트에 점이 있다는 것만 보면
|
||||||
| Some i -> String.contains (String.sub path 0 i) '.'
|
".."이 패키지로 오인된다 — 그러면 import가 조용히 해소되지 않고, 그
|
||||||
| None -> String.contains path '.'
|
모듈의 이름이 전부 불투명해져 검사가 통째로 공허해진다. *)
|
||||||
|
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/"
|
let std_prefix = "cool.dev/std/"
|
||||||
|
|
||||||
|
|||||||
+31
-1
@@ -43,6 +43,26 @@ type env = {
|
|||||||
|
|
||||||
let err env pos msg = env.errors <- { pos; msg } :: env.errors
|
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 =
|
let mismatch env pos expected got what =
|
||||||
err env pos
|
err env pos
|
||||||
(Printf.sprintf "%s: %s이(가) 필요한데 %s입니다" what (T.show expected) (T.show got))
|
(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
|
go b.stmts
|
||||||
|
|
||||||
and check_stmt env = function
|
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; _ } ->
|
| S_let { pat; ty; value; pos; _ } ->
|
||||||
let declared = Option.map (conv env []) ty in
|
let declared = Option.map (conv env []) ty in
|
||||||
let got =
|
let got =
|
||||||
@@ -668,7 +699,6 @@ and check_stmt env = function
|
|||||||
let p = infer env place in
|
let p = infer env place in
|
||||||
let v = infer env value in
|
let v = infer env value in
|
||||||
if not (T.unify p v) then mismatch env pos p v "대입"
|
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}/samples/*.cool)
|
||||||
(glob_files %{workspace_root}/std/*.cool)
|
(glob_files %{workspace_root}/std/*.cool)
|
||||||
(glob_files %{workspace_root}/docs/*.ebnf)
|
(glob_files %{workspace_root}/docs/*.ebnf)
|
||||||
|
(glob_files_rec %{workspace_root}/dogfoods/**)
|
||||||
(glob_files %{workspace_root}/samples/app/*)
|
(glob_files %{workspace_root}/samples/app/*)
|
||||||
(glob_files %{workspace_root}/samples/modules/*.cool)
|
(glob_files %{workspace_root}/samples/modules/*.cool)
|
||||||
(glob_files %{workspace_root}/samples/run/*.cool)))
|
(glob_files %{workspace_root}/samples/run/*.cool)))
|
||||||
|
|||||||
@@ -1653,3 +1653,58 @@ let () =
|
|||||||
check "자식의 crash가 scope 밖으로 전파된다" false;
|
check "자식의 crash가 scope 밖으로 전파된다" false;
|
||||||
check "crash 전의 출력은 남는다" false;
|
check "crash 전의 출력은 남는다" false;
|
||||||
check "scope 뒤는 실행되지 않는다" 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