dogfoods: LRU 캐시 — 비렉시컬 정리, 그리고 Option/Result 패턴의 큰 버그
파일과 풀은 정리 지점이 렉시컬이었다. LRU는 아니다 — 축출은 삽입의 부작용으로
예측할 수 없는 때에 일어난다. with를 구현하기 전에 알아야 할 자리였다.
D10 (큰 것) — Option/Result 패턴이 타입을 통째로 잃고 있었다.
match r { Ok(v) => v, Err(e) => e } // Int를 String 자리에 두는데 통과
typecheck의 env.ctors에 내장 생성자가 없어서 None이 "None이라는 이름의 변수
바인딩"이 되고 Ok(v)의 v가 TUnknown이 됐다. 즉 이 언어의 핵심 오류 처리
수단의 패턴 매칭이 타입을 전혀 검사하지 않고 있었다. 소진성 검사는 자기
is_ctor에 내장을 갖고 있어 이 사실을 덮었다 — 빠진 경우는 잡으면서 타입은
안 봤다. 내장 열거형을 등록해 고쳤고 기존 코드는 하나도 안 깨졌다.
여태 안 보인 이유: Ok(v) => Int.show(v) 같은 정상 코드는 v가 TUnknown이어도
통과하므로 아무도 이상함을 못 느낀다. 틀린 코드를 써봐야 드러난다.
D11 — struct 필드에서 affine 값을 꺼낼 수 없다. 필드 접근은 빌림이고 struct
분해 패턴이 없다. 열거형은 패턴이 분해하므로 가능하다. 즉 "둘을 함께
돌려주기"가 열거형으로만 된다. 튜플이 없는 대가가 표현 가능성 문제로 나왔다.
D12 — 축출은 렉시컬이 아니다. 그래서 자원이 렉시컬 전용이면 자원 컨테이너를
언어로 만들 수 없다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한
런타임 capability인 이유가 만들 수 없어서였다. (a) 렉시컬 전용을 택하고,
사용자가 자기 자원 풀을 못 만든다는 대가를 기록한다.
자료구조 자체의 마찰: Map이 없어 O(n), 튜플이 없어 운반용 struct 세 개,
get이 캐시를 새로 돌려줘야 함, 뒤에서 자르는 std 함수 없음.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
@@ -502,6 +502,15 @@ prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌
|
|||||||
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
|
정리에 다른 값이 필요하면 with가 자동으로 부를 수 없기 때문이다.
|
||||||
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
|
고침: 함수 타입과 클로저 파라미터의 own. 그 구멍이 std의 fold가 틀린 것을
|
||||||
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
|
덮고 있었다 — 누적자는 매 단계 소비되므로 own이다.
|
||||||
|
LRU 캐시가 제약 하나를 더했다: 축출은 렉시컬이 아니므로 with가 못 닿는다.
|
||||||
|
따라서 자원은 렉시컬 전용으로 간다 — 자원을 담는 컨테이너(풀, 캐시,
|
||||||
|
레지스트리)는 런타임만 만들 수 있고, 사용자는 언어로 못 만든다. 그 대가를
|
||||||
|
알고 택한다. 이미 그렇게 되어 있었다 — std-draft의 Pool이 불투명한 런타임
|
||||||
|
capability인 이유가 만들 수 없어서였다.
|
||||||
|
그리고 큰 버그 하나를 잡았다: Option/Result의 패턴이 타입을 통째로 잃고
|
||||||
|
있었다. 내장 생성자가 typecheck에 등록되지 않아 None이 변수 바인딩이 되고
|
||||||
|
Ok(v)의 v가 TUnknown이었다. Ok(v) => v 로 Int를 String 자리에 두는 코드가
|
||||||
|
통과했다. 소진성 검사는 자기 목록에 내장을 갖고 있어 이 사실을 덮고 있었다.
|
||||||
전문은 dogfoods/FINDINGS.md.
|
전문은 dogfoods/FINDINGS.md.
|
||||||
|
|
||||||
■ 개밥 먹기 1 — samples/app (2026-08)
|
■ 개밥 먹기 1 — samples/app (2026-08)
|
||||||
|
|||||||
@@ -282,6 +282,102 @@ resource Lease {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## LRU 캐시 (2026-08-30)
|
||||||
|
|
||||||
|
목적: 앞의 둘이 못 건드린 데를 친다. 파일과 풀은 정리 지점이 **렉시컬**이었다.
|
||||||
|
LRU는 아니다 — **축출은 삽입의 부작용으로 예측할 수 없는 때에 일어난다.**
|
||||||
|
|
||||||
|
산출물: `lru-cache/values.cool` 82줄, `lru-cache/resources.cool` 73줄.
|
||||||
|
|
||||||
|
### D10. Option/Result 패턴이 타입을 통째로 잃고 있었다 — 고침. 큰 것이었다
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub fn probe3(r: Result[Int, String]) -> String {
|
||||||
|
match r {
|
||||||
|
Ok(v) => v, // Int를 String 자리에 둔다
|
||||||
|
Err(e) => e,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**통과했다.** 원인: typecheck의 `env.ctors`에 내장 생성자가 없었다. 그래서
|
||||||
|
`None`은 생성자가 아니라 **`None`이라는 이름의 변수 바인딩**이 되고,
|
||||||
|
`Ok(v)`의 `v`는 `TUnknown`이 되어 무엇과도 맞았다.
|
||||||
|
|
||||||
|
즉 **이 언어의 핵심 오류 처리 수단인 Option/Result의 패턴 매칭이 타입을 전혀
|
||||||
|
검사하지 않고 있었다.** 소진성 검사는 자기 `is_ctor`에 내장을 갖고 있어서
|
||||||
|
이 사실을 덮고 있었다 — 빠진 경우는 잡으면서 타입은 안 봤다.
|
||||||
|
|
||||||
|
내장 열거형을 등록해서 고쳤다. 기존 코드는 하나도 안 깨졌다.
|
||||||
|
|
||||||
|
이 버그가 여태 안 보인 이유가 씁쓸하다. `match r { Ok(v) => Int.show(v) }`
|
||||||
|
같은 코드는 v가 TUnknown이어도 통과하므로 **아무도 이상함을 못 느낀다.**
|
||||||
|
틀린 코드를 써봐야 드러난다.
|
||||||
|
|
||||||
|
### D11. struct 필드에서 affine 값을 꺼낼 수 없다
|
||||||
|
|
||||||
|
`put`은 캐시와 축출된 자원을 **함께** 돌려줘야 한다. 튜플이 없으니 struct다.
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub struct Put { cache: LeaseCache, evicted: Option[Db.Lease] }
|
||||||
|
...
|
||||||
|
match p.evicted { Some(l) => release(l), ... }
|
||||||
|
```
|
||||||
|
```
|
||||||
|
l은(는) 빌린 값이라 다른 함수에 넘길 수 없습니다
|
||||||
|
```
|
||||||
|
|
||||||
|
**필드 접근은 빌림이고, struct를 분해하는 패턴이 언어에 없다.** 그래서 struct에
|
||||||
|
넣은 affine 값은 다시 꺼낼 수 없다.
|
||||||
|
|
||||||
|
열거형은 된다 — 패턴이 분해하기 때문이다:
|
||||||
|
|
||||||
|
```cool
|
||||||
|
pub enum Put {
|
||||||
|
Kept(LeaseCache),
|
||||||
|
Evicted(LeaseCache, Db.Lease),
|
||||||
|
}
|
||||||
|
match p { Evicted(c, l) => release_then(db, pool, l, c), ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
**즉 "둘을 함께 돌려주기"가 열거형으로만 가능하다.** 튜플이 없는 대가가
|
||||||
|
여기서 두 번째로 나온다(F5에 이어). 그리고 이건 취향 문제가 아니라
|
||||||
|
**표현 가능성 문제**다.
|
||||||
|
|
||||||
|
→ 해법 후보: struct 분해 패턴 + 부분 이동, 또는 튜플. 둘 다 문법 결정이다.
|
||||||
|
|
||||||
|
### D12. 축출은 렉시컬이 아니다 — `with`가 못 닿는 첫 자리
|
||||||
|
|
||||||
|
`with`는 렉시컬 수명만 다룬다. 축출된 lease는 **어느 블록에도 묶이지 않는다.**
|
||||||
|
삽입할 때 튀어나오고, 그 시점은 캐시 상태에 달려 있다.
|
||||||
|
|
||||||
|
그래서 `resource`가 렉시컬 전용이면 **자원을 담는 컨테이너를 언어로 만들 수
|
||||||
|
없다.** 그리고 이것이 이미 우리 설계에 나타나 있었다 — `Pool` 자체가 자원
|
||||||
|
캐시인데, `std-draft/db.cool`에서 **런타임이 주는 불투명한 capability**로
|
||||||
|
선언했다. 만들 수 없어서 그렇게 한 것이다.
|
||||||
|
|
||||||
|
두 갈래다:
|
||||||
|
|
||||||
|
- **(a) 자원은 렉시컬 전용** → 자원 컨테이너는 런타임만 만들 수 있다.
|
||||||
|
풀, 캐시, 레지스트리가 전부 언어 밖이 된다. 단순하고, 지금 상태가 그렇다
|
||||||
|
- **(b) 자원이 자료구조로 탈출할 수 있다** → `with`만으로 부족하고 결국
|
||||||
|
선형 타입이 필요하다. v0가 미룬 바로 그것
|
||||||
|
|
||||||
|
**(a)를 권한다.** 자원 컨테이너는 드물고, 만드는 쪽은 런타임이며, 쓰는 쪽은
|
||||||
|
lease를 렉시컬하게 빌린다. 다만 **이 선택이 무엇을 포기하는지 적어둬야
|
||||||
|
한다** — 사용자가 자기 자원 풀을 언어로 못 만든다.
|
||||||
|
|
||||||
|
### 자료구조 자체의 마찰 (값 판)
|
||||||
|
|
||||||
|
- **Map이 없어 O(n)이다.** LRU의 요점이 O(1)인데 표현할 수단이 없다
|
||||||
|
- **튜플이 없어 운반용 struct를 세 번 만들었다** — `Got`, `Put`, `Dropping`
|
||||||
|
- **`get`이 캐시를 새로 돌려줘야 한다.** 최근성이 바뀌므로. 공유 가변 상태가
|
||||||
|
없다는 것의 대가이고, 호출자가 캐시를 계속 실어날라야 한다
|
||||||
|
- **뒤에서 자르는 함수가 std에 없다.** `drop_last`를 `reverse` 두 번으로 썼다
|
||||||
|
- **fold에 "첫 원소 건너뛰기"가 없어** `Dropping` struct를 또 만들었다
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 결정 — resource / with
|
## 결정 — resource / with
|
||||||
|
|
||||||
**넣는다. 다만 D4가 먼저다.**
|
**넣는다. 다만 D4가 먼저다.**
|
||||||
@@ -303,6 +399,10 @@ resource Lease {
|
|||||||
(자원을 필드로 가진 타입은 전이적으로 자원)
|
(자원을 필드로 가진 타입은 전이적으로 자원)
|
||||||
3. 그 뒤 Mini Shell로 세 번째 데이터 점
|
3. 그 뒤 Mini Shell로 세 번째 데이터 점
|
||||||
|
|
||||||
|
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
|
||||||
|
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
|
||||||
|
사용자가 자기 자원 풀을 언어로 못 만드는 것이 그 대가다.
|
||||||
|
|
||||||
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
|
**미루는 것**: crash 시 정리 여부. `with`를 넣어도 이 질문은 열려 있다 —
|
||||||
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
|
정상 종료·`return`·`?`에서는 돌고 crash에서는 안 도는 것이 지금의 잠정
|
||||||
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
|
답이지만, 감독(supervision)이 들어오면 다시 봐야 한다.
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ run ✗
|
|||||||
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
|
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
|
||||||
| 5 | TCP Server | 규모의 API 모양 | 대기 |
|
| 5 | TCP Server | 규모의 API 모양 | 대기 |
|
||||||
| 6 | DB Pool | lease, 자원 정리 | **완료** |
|
| 6 | DB Pool | lease, 자원 정리 | **완료** |
|
||||||
|
| + | **LRU Cache** | **비렉시컬 정리 (축출)** | **완료** |
|
||||||
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
|
| 7 | Resource Exhaustion | 실패 의미론의 최악 조건 | **미룸** — 런타임 속성이라 안 돌리면 알 수 없다. 언어 층 질문("무엇이 Result이고 무엇이 crash인가")은 std-draft 규율로 흡수했다 |
|
||||||
|
|
||||||
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
|
순서는 자원 모델(2, 6, 1) → 동시성·취소(3, 4, 5)다. 2번을 먼저 둔 이유는
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// LRU 캐시 — 자원 판.
|
||||||
|
//
|
||||||
|
// 앞의 두 개밥(파일, 풀)은 정리 지점이 렉시컬이었다. 여기는 아니다.
|
||||||
|
// 축출은 삽입의 부작용으로, 예측할 수 없는 때에 일어난다.
|
||||||
|
//
|
||||||
|
// 이것이 with 결정 전에 알아야 할 자리다 — with는 렉시컬 수명만 다루는데
|
||||||
|
// 축출된 자원은 어느 블록에도 묶이지 않는다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
import "../std-draft/db" as Db
|
||||||
|
|
||||||
|
pub struct Held {
|
||||||
|
key: String,
|
||||||
|
lease: Db.Lease,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lease를 필드로 가지므로 전이적으로 affine이다 — 선언하지 않아도 유도된다.
|
||||||
|
pub struct LeaseCache {
|
||||||
|
cap: Int,
|
||||||
|
held: List[Held],
|
||||||
|
}
|
||||||
|
|
||||||
|
// put은 캐시와 "떨어져 나온 자원"을 함께 돌려줘야 한다. 튜플이 없다.
|
||||||
|
//
|
||||||
|
// struct로 쓰면 막힌다 — 필드 접근은 빌림이라 affine 값을 꺼낼 수 없고,
|
||||||
|
// struct를 분해하는 패턴이 언어에 없다. 열거형은 패턴이 분해하므로 꺼낼 수
|
||||||
|
// 있다. 그래서 "둘을 함께 돌려주기"가 열거형으로만 가능하다.
|
||||||
|
pub enum Put {
|
||||||
|
Kept(LeaseCache),
|
||||||
|
Evicted(LeaseCache, Db.Lease),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(cap: Int) -> LeaseCache {
|
||||||
|
LeaseCache { cap: cap, held: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 넣는다. 넘치면 가장 오래된 것이 떨어져 나온다.
|
||||||
|
//
|
||||||
|
// 떨어져 나온 lease는 호출자가 반납해야 한다. 언어는 그것을 강제하지 못한다 —
|
||||||
|
// Put.evicted를 무시하고 버려도 아무 말이 없다. 누수 허용이므로.
|
||||||
|
pub fn put(own c: LeaseCache, key: String, own l: Db.Lease) -> Put
|
||||||
|
|
||||||
|
// 호출자가 해야 하는 일. 이 함수를 안 부르면 조용히 샌다.
|
||||||
|
pub fn drain(db: Db.Db, pool: Db.Pool, own p: Put)
|
||||||
|
effects {Db.release} -> LeaseCache {
|
||||||
|
match p {
|
||||||
|
Kept(c) => c,
|
||||||
|
Evicted(c, l) => release_then(db, pool, l, c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn release_then(
|
||||||
|
db: Db.Db,
|
||||||
|
pool: Db.Pool,
|
||||||
|
own l: Db.Lease,
|
||||||
|
own c: LeaseCache,
|
||||||
|
) effects {Db.release} -> LeaseCache {
|
||||||
|
let _ = db.release(pool, l)
|
||||||
|
c
|
||||||
|
}
|
||||||
|
|
||||||
|
// 쓰는 쪽. 매 삽입마다 축출을 받아 처리해야 한다.
|
||||||
|
pub fn add(db: Db.Db, pool: Db.Pool, own c: LeaseCache, key: String)
|
||||||
|
effects {Db.acquire, Db.release} -> Result[LeaseCache, Db.DbError] {
|
||||||
|
match db.acquire(pool) {
|
||||||
|
Err(e) => Err(e),
|
||||||
|
Ok(l) => Ok(drain(db, pool, put(c, key, l))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn size(c: LeaseCache) -> Int {
|
||||||
|
List.len(c.held)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// LRU 캐시 — 값 판.
|
||||||
|
//
|
||||||
|
// 먼저 자원 없이 써서 자료구조 자체의 마찰을 본다. 자원 판은 resources.cool.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
|
||||||
|
// ※ Option[Int]의 Option은 내장 타입이라 import가 필요 없다.
|
||||||
|
|
||||||
|
pub copyable struct Entry {
|
||||||
|
key: String,
|
||||||
|
value: Int,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 최근에 쓴 것이 앞. Map이 없어 리스트를 훑는다 — O(n)이다.
|
||||||
|
// LRU의 요점이 O(1)인데 그것을 표현할 수단이 언어에 없다.
|
||||||
|
pub copyable struct Cache {
|
||||||
|
cap: Int,
|
||||||
|
entries: List[Entry],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn empty(cap: Int) -> Cache {
|
||||||
|
Cache { cap: cap, entries: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
// get이 최근성을 바꾸므로 캐시를 새로 돌려줘야 한다.
|
||||||
|
// 그런데 값도 같이 돌려줘야 한다 — 튜플이 없어 struct를 하나 더 만든다.
|
||||||
|
pub copyable struct Got {
|
||||||
|
cache: Cache,
|
||||||
|
value: Option[Int],
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(c: Cache, key: String) -> Got {
|
||||||
|
let hit = List.first(List.filter(c.entries, fn(e) { e.key == key }))
|
||||||
|
match hit {
|
||||||
|
None => Got { cache: c, value: None },
|
||||||
|
Some(e) => Got {
|
||||||
|
cache: Cache { cap: c.cap, entries: touch(c.entries, key, e) },
|
||||||
|
value: Some(e.value),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 찾은 항목을 앞으로 옮긴다.
|
||||||
|
pub fn touch(entries: List[Entry], key: String, e: Entry) -> List[Entry] {
|
||||||
|
List.concat([e], List.filter(entries, fn(x) { !(x.key == key) }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn put(c: Cache, key: String, value: Int) -> Cache {
|
||||||
|
let without = List.filter(c.entries, fn(x) { !(x.key == key) })
|
||||||
|
let added = List.concat([Entry { key: key, value: value }], without)
|
||||||
|
Cache { cap: c.cap, entries: evict(added, c.cap) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 넘치면 뒤에서 떨어뜨린다. 값이라 그냥 사라진다 — 자원이면 이야기가 다르다.
|
||||||
|
pub fn evict(entries: List[Entry], cap: Int) -> List[Entry] {
|
||||||
|
if List.len(entries) <= cap {
|
||||||
|
entries
|
||||||
|
} else {
|
||||||
|
evict(drop_last(entries), cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 마지막 하나를 뺀 리스트. 뒤에서 자르는 함수가 std에 없다.
|
||||||
|
pub fn drop_last(entries: List[Entry]) -> List[Entry] {
|
||||||
|
List.reverse(drop_first(List.reverse(entries)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn drop_first(entries: List[Entry]) -> List[Entry] {
|
||||||
|
List.fold(entries, Dropping { first: true, kept: [] }, fn(own d, e) {
|
||||||
|
if d.first {
|
||||||
|
Dropping { first: false, kept: d.kept }
|
||||||
|
} else {
|
||||||
|
Dropping { first: false, kept: List.push(d.kept, e) }
|
||||||
|
}
|
||||||
|
}).kept
|
||||||
|
}
|
||||||
|
|
||||||
|
// fold에 "첫 원소 건너뛰기"가 없어 운반용 struct를 또 만든다.
|
||||||
|
pub copyable struct Dropping {
|
||||||
|
first: Bool,
|
||||||
|
kept: List[Entry],
|
||||||
|
}
|
||||||
@@ -834,6 +834,18 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
|||||||
errors = [];
|
errors = [];
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
|
(* 내장 열거형. 이것이 없으면 Option/Result의 패턴이 생성자로 인식되지
|
||||||
|
않고 변수 바인딩이 되어, match 안에서 타입 정보가 통째로 사라진다.
|
||||||
|
사용자 정의가 덮을 수 있도록 먼저 넣는다. *)
|
||||||
|
Hashtbl.replace env.enums "Option"
|
||||||
|
([ "a" ], [ ("Some", [ T.TVar "a" ]); ("None", []) ]);
|
||||||
|
Hashtbl.replace env.enums "Result"
|
||||||
|
([ "a"; "e" ], [ ("Ok", [ T.TVar "a" ]); ("Err", [ T.TVar "e" ]) ]);
|
||||||
|
List.iter
|
||||||
|
(fun (v, e) -> Hashtbl.replace env.ctors v e)
|
||||||
|
[
|
||||||
|
("Some", "Option"); ("None", "Option"); ("Ok", "Result"); ("Err", "Result");
|
||||||
|
];
|
||||||
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
|
(* 1차: 타입과 생성자 이름부터 (선언 순서에 의존하지 않는다) *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun it ->
|
(fun it ->
|
||||||
|
|||||||
@@ -1708,3 +1708,27 @@ let () =
|
|||||||
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
|
let st = Session.create ~root:"../dogfoods/atomic-update" ~std:"../std" () in
|
||||||
Session.load st "../dogfoods/atomic-update/naive.cool";
|
Session.load st "../dogfoods/atomic-update/naive.cool";
|
||||||
check "누수는 아직 오류가 아니다 (v0 정책)" (Session.errors st = [])
|
check "누수는 아직 오류가 아니다 (v0 정책)" (Session.errors st = [])
|
||||||
|
|
||||||
|
(* D10 회귀: Option/Result 패턴이 타입을 나른다.
|
||||||
|
전에는 내장 생성자가 typecheck의 ctors에 없어 None이 변수 바인딩이 되고
|
||||||
|
Ok(v)의 v가 TUnknown이었다 — 즉 핵심 오류 처리 수단의 패턴 매칭이 타입을
|
||||||
|
전혀 검사하지 않았다. *)
|
||||||
|
let () =
|
||||||
|
check "Ok(v)의 v가 타입을 갖는다"
|
||||||
|
(type_has
|
||||||
|
"fn f(r: Result[Int, String]) -> String {\n\
|
||||||
|
\ match r {\n\
|
||||||
|
\ Ok(v) => v,\n\
|
||||||
|
\ Err(e) => e,\n\
|
||||||
|
\ }\n\
|
||||||
|
}"
|
||||||
|
"String이(가) 필요한데 Int");
|
||||||
|
check "None은 변수 바인딩이 아니다"
|
||||||
|
(type_has
|
||||||
|
"fn f(o: Option[Int]) -> Int {\n\
|
||||||
|
\ match o {\n\
|
||||||
|
\ None => \"x\",\n\
|
||||||
|
\ Some(v) => v,\n\
|
||||||
|
\ }\n\
|
||||||
|
}"
|
||||||
|
"match 팔의 타입이 서로 다릅니다")
|
||||||
|
|||||||
Reference in New Issue
Block a user