Process Supervisor: 실패 위상, 그리고 이름 한정의 구멍 둘

문서의 주장 셋이 코드가 됐다:
- spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다
- 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다.
  lease를 들고 가는 워커는 supervise에 못 간다
- 격리 경계는 recover가 아니다 — Death에 담을 수 있는 것이 메시지뿐이다

D19: 가져온 모듈의 struct를 만들 수 없었다. 타입은 쓸 수 있는데 값은 못
만드는 상태였고, 개밥 넷이 같은 모듈 안에서만 struct를 만들어 안 걸렸다.
고치는 동안 세 검증 장치가 각각 다른 실수를 잡았다 — LL(1)이 점을 두
군데서 먹는 것을, 문법-파서 대조가 f().x { }를, 생성 커버리지가 밟히지
않는 프로덕션을.

D20: 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다.
- 패턴에서는 바인딩이 되어 그 팔이 전부를 잡고 오류가 다음 팔에 났다.
  틀린 줄을 가리키는 진단은 없느니만 못하다
- 식에서는 모르는 이름이 되어 move 검사가 생성자인 줄 몰랐다.
  Wrap(f)를 두 번 써서 affine 값을 복제할 수 있었다 (보안 정리 ii)

Mini Shell에 다섯 군데 있었다 — 그 개밥이 시험하려던 파이프 처리 전체가
검사되지 않고 있었다. 고친 뒤에야 ReadFd/WriteFd 구분과 fd의 이중 소비가
잡힌다. D1 → D15 → D20이 같은 병의 세 단계다: 모듈 전체 → 점 왼쪽 →
점을 안 찍은 이름.

D21(열림): spawn한 태스크의 값이 갈 데가 없다. 보고를 Log로 한다.
취소·backpressure 전에 결정해야 한다.

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 20:15:39 +09:00
co-authored by Claude Opus 5
parent 6bfdcaa0d6
commit 1baef87b2e
14 changed files with 502 additions and 30 deletions
+13 -1
View File
@@ -284,7 +284,19 @@ crash_expr = "crash" , "(" , expr , ")" ;
(* ident struct . "{"
* *)
name_or_struct<yes> = ident , [ struct_body ] ;
(* struct . Alias.Name { ... }
* postfix "{" ,
* struct .
* "." *)
(* Alias.Name { ... } struct .
* .
*
* struct postfix : "{"
* , "{" .
* struct (<yes>) .
* <no> if x.y { } "{" *)
name_or_struct<yes> = ident , ( "." , ident , [ struct_body ]
| [ struct_body ] ) ;
name_or_struct<no> = ident ;
struct_body = "{" , brace_list<field_init> , "}" ;
field_init = ident , ":" , expr ;
+121
View File
@@ -625,6 +625,125 @@ fn spawn(program: String, args: List[String],
- **`own self` 뒤로 `child.wait()`가 자연스럽다.** 1판 문법이었다면
`os.wait(child)`였고 `Os` 권한을 자식 거두기까지 들고 다녀야 했다
## Process Supervisor (2026-08-30)
목적: 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을
때 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
산출물: `std-draft/sup.cool` 60줄, `supervisor/workers.cool` 130줄.
### 확인된 것 — 문서의 주장 셋이 코드가 됐다
**① spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다.**
`sc.spawn(f)`는 실패가 부모로 가고 `sup.supervise(sc, f, policy)`는 여기서
멈춘다. 실패를 삼키려면 `Supervisor`를 받았어야 하고 **그것이 시그니처에
보인다.** 문법 변경 없이 성립한다.
**② 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다.**
이것이 이번 개밥의 가장 값싼 확인이다. `supervise`의 body가 무표기 `fn`이므로:
```
affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)
```
lease를 들고 가는 워커는 `supervise`에 못 간다. **한 번밖에 못 도는 것을
재시작할 수는 없다** — 얼랭이 관례로 지키는 것을 타입이 막는다. 한 번만
돌리는 `once``affine fn`을 받으므로 그런 워커도 받는다.
그래서 재시작 가능한 워커의 모양이 정해진다: **자원을 밖에서 받지 말고
안에서 획득하고 안에서 반납한다.** 풀을 빌려 받을 뿐 lease를 들고 오지
않으므로 클로저가 affine이 아니다.
**③ 격리 경계는 recover가 아니다.** `Death`에 담을 수 있는 것이 메시지뿐이다.
죽은 일꾼이 계산하던 값도, 들고 있던 자원도 넘어오지 않는다. **담을 자리가
있으면 그것이 recover다** — 이 열거형의 모양이 그 원칙의 코드 수준 의미다.
### D19. 가져온 모듈의 struct를 만들 수 없었다 — 고침
```cool
sup.supervise(sc, body, Sup.Policy { retries: 3 })
^ )이(가) 필요합니다 — { 발견
```
`Alias.Name { ... }`가 문법에 없었다. **타입은 쓸 수 있는데 값은 못 만드는
상태**였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어 한 번도 안
걸렸다.
고치는 과정이 문법 검증 장치의 값을 보여줬다. 처음에 점을 `field_sfx`
`name_or_struct` 양쪽에 두자 **LL(1) 검사가 즉시 진짜 충돌 2건을 냈다**
같은 문장을 두 프로덕션이 읽을 수 있었다. 다음 시도(struct_body를 field_sfx에
붙이기)는 LL(1)은 통과했지만 **문법-파서 대조와 생성 커버리지가 잡았다**:
문법은 `f().x { }`를 만드는데 파서가 거부했다. 최종형은 struct를 만들 수 있는
문맥(`<yes>`)에서만 점을 미리 먹는다. 세 장치가 각각 다른 실수를 잡았다.
### D20. 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다 — 고침 (보안)
두 자리에서 각각 다르게 샜다.
**패턴에서는 바인딩이 됐다.**
```cool
match s {
Done => ..., // Sup.Done이어야 한다. 여기서는 그냥 이름 하나다
GaveUp(d) => ..., // "이 팔은 도달할 수 없습니다" — 틀린 줄을 가리킨다
}
```
규칙 자체는 옳다 ("한정되었으면 언제나 생성자, 아니면 바인딩"). 문제는
**진단이 엉뚱한 줄을 가리킨다**는 것이다. 첫 팔이 전부를 잡는 것이 원인인데
오류는 둘째 팔에 난다. 이제 이렇게 말한다:
```
Done은(는) 가져온 열거형의 variant입니다 — Sup.Done로 적으십시오
(한정 없는 이름은 바인딩입니다)
```
**식에서는 모르는 이름이 됐다. 이쪽이 보안 구멍이다.**
```cool
fn dup(own f: K.Fd) -> K.Box {
let a = Wrap(f)
Wrap(f) // 통과했다
}
```
`Wrap`이 생성자인 줄 모르므로 move 검사의 `is_ctor`가 안 걸리고, 인자가
Move가 아니라 빌림이 된다. **affine 값이 복제된다.** D13(생성자가 인자를
소비하지 않았다)을 고쳤는데, 가져온 생성자는 애초에 생성자로 인식되지도
않고 있었다.
**Mini Shell에 다섯 군데 있었다.** `Ends(r, w)`, `ToPipe(w)`, `FromPipe(r)`,
`InheritIn`, `InheritOut` — 즉 **이 개밥이 시험하려던 파이프 처리 전체가
검사되지 않고 있었다.** 고친 뒤 확인하니 이제 진짜로 잡는다:
```
인자: Os.ReadFd이(가) 필요한데 Os.WriteFd입니다
w은(는) 이미 move되었습니다 (81:81에서 소비)
```
**이것이 개밥 먹기가 통과했다는 사실을 믿으면 안 되는 이유다.** 합격 기준 ②
("모듈이 실제로 해소될 것")는 모듈 수준의 공허함만 막는다. **이름 하나가
조용히 불투명해지는 것은 못 막는다.** D1 → D15 → D20이 같은 병의 세 단계였다:
모듈 전체 → 점 왼쪽 → 점을 안 찍은 이름.
### D21. spawn한 태스크의 값이 갈 데가 없다 — 열림
```cool
work.spawn(fn() { announce(log, run_worker(...)) })
```
일꾼이 어떻게 끝났는지 부모가 알 수 없다. 클로저의 반환값이 갈 데가 없기
때문이다 — `Future``Task[a]`도 없고, `scope`의 join은 값을 만들지 않는다.
그래서 보고를 값이 아니라 **Log capability**로 한다. 얼랭도 그렇게 하므로
틀린 모양은 아니고, 오히려 "결과는 effect로 나간다"가 감독과 잘 맞는다.
다만 **취소와 backpressure에 가면 이것으로 안 될 가능성이 높다** — 되돌려
받을 값이 있어야 흐름을 조절할 수 있다.
Reverse Proxy(#4) 전에 결정해야 한다.
---
---
## 테스트 하네스의 결함 (2026-08-30)
@@ -649,6 +768,8 @@ fn spawn(program: String, args: List[String],
1. ~~**D4** — 수신자를 소비하는 메서드~~ → 고침. `own self`
2. **`resource` 종류와 `with`** — 아래 "with를 미루는 이유" 참고
3. ~~Mini Shell~~ → 완료. D13–D18이 여기서 나왔다
4. ~~Process Supervisor~~ → 완료. D19–D21. 문서의 주장 셋이 코드가 됐다
5. 다음은 Reverse Proxy(#4). 그 전에 D21(태스크의 값)을 결정해야 한다
**LRU가 더한 제약**: 자원은 렉시컬 전용으로 간다. 자원을 담는 컨테이너는
런타임만 만들 수 있다 — 축출처럼 비렉시컬한 정리는 `with`가 못 닿기 때문이다.
+1 -1
View File
@@ -59,7 +59,7 @@ run ✗
|---|---|---|---|
| 1 | **Mini Shell** | **여러 자원, 하나가 둘로 갈라짐** | **완료** |
| 2 | **Atomic File Updater** | **자원 수명, 실패 경로** | **완료** |
| 3 | Process Supervisor | 실패 위상 | 대기 |
| 3 | **Process Supervisor** | **실패 위상** | **완료** |
| 4 | Reverse Proxy | 취소, backpressure | 대기 |
| 5 | TCP Server | 규모의 API 모양 | 대기 |
| 6 | DB Pool | lease, 자원 정리 | **완료** |
+4 -4
View File
@@ -27,7 +27,7 @@ pub fn run(os: Os.Os, p: Parse.Pipeline)
if List.is_empty(p.stages) {
Ok(0)
} else {
from(os, p.stages, InheritIn)
from(os, p.stages, Os.InheritIn)
}
}
@@ -56,7 +56,7 @@ pub fn from(os: Os.Os, stages: List[Parse.Command], own stdin: Os.Stdin)
// 마지막 단계. 출력은 부모 것을 그대로 쓴다.
pub fn last(os: Os.Os, c: Parse.Command, own stdin: Os.Stdin)
effects {Os.spawn, Child.wait} -> Result[Int, Os.OsError] {
match os.spawn(c.program, c.args, stdin, InheritOut) {
match os.spawn(c.program, c.args, stdin, Os.InheritOut) {
Err(e) => Err(e),
Ok(child) => child.wait(),
}
@@ -78,13 +78,13 @@ pub fn middle(
// stdin이 샌다
Err(e) => Err(e),
Ok(p) => match p {
Ends(r, w) => match os.spawn(c.program, c.args, stdin, ToPipe(w)) {
Os.Ends(r, w) => match os.spawn(c.program, c.args, stdin, Os.ToPipe(w)) {
// 여기서는 r이 샌다. w는 spawn이 가져갔는지 아닌지
// 시그니처만 봐서는 알 수 없다 — 아래 발견 참고.
Err(e) => Err(e),
// 자식이 떴다. 부모는 쓰는 끝을 이미 넘겼고, 읽는 끝을
// 다음 단계에 넘긴다. 부모에게는 아무것도 안 남는다.
Ok(child) => reap(child, from(os, rest, FromPipe(r))),
Ok(child) => reap(child, from(os, rest, Os.FromPipe(r))),
},
},
}
+56
View File
@@ -0,0 +1,56 @@
// 감독 — 개밥 먹기용 초안.
//
// 문서의 주장을 코드로 옮겨 본다: "spawn은 하나의 개념이고 실패가 어디까지
// 가느냐가 권한으로 갈린다." 그 말이 실제 시그니처가 되는지가 이 파일의 목적이다.
//
// 규율은 앞의 셋과 같다. 없는 언어 기능에 기대면 그것이 발견이다.
// 죽었다는 사실. 값이 아니라 사실만 넘어온다.
//
// 격리 경계는 recover가 아니다 — 실패한 계산이 만든 값은 경계를 넘지
// 못하고, 경계가 얻는 것은 죽었다는 것과 메시지뿐이다. 그래서 이 열거형에
// 자식의 결과를 담는 자리가 없다. 담을 자리가 있으면 그것이 recover다.
pub enum Death {
Crashed(String),
}
// 왜 멈췄는가. 감독자가 사람에게 보고할 것.
pub enum Stopped {
// 스스로 끝났다
Done,
// 죽었고 다시 안 띄운다
GaveUp(Death),
}
// 재시작 정책. 얼랭의 OTP가 라이브러리인 것과 같은 이유로 값이다 —
// 언어가 정할 일이 아니다.
pub copyable struct Policy {
// 몇 번까지 다시 띄우는가. 0이면 한 번 죽으면 끝이다.
retries: Int,
}
// 감독할 권한.
//
// TaskScope와 따로 있는 것이 요점이다. sc.spawn(f)는 실패가 부모로
// 전파되고, sup.supervise(sc, f)는 여기서 멈춘다. **실패를 삼키려면
// Supervisor를 받았어야 하고, 그것이 시그니처에 보인다.**
pub capability Supervisor {
// 죽으면 다시 띄운다.
//
// body가 무표기 fn인 것이 이 시그니처의 전부다. 무표기 fn은 여러 번
// 부를 수 있고, affine 값을 capture한 클로저는 여기 못 온다 —
// 한 번밖에 못 도는 것을 재시작할 수는 없기 때문이다.
// 얼랭이 관례로 지키는 것을 affinity가 검사한다.
fn supervise[e: effects](
sc: TaskScope,
body: fn() effects e,
policy: Policy,
) effects {Supervisor.supervise} | e -> Stopped
// 한 번만 돌린다. 죽으면 그것으로 끝이고 사실만 돌려준다.
// 재시작이 없으므로 자원을 들고 가는 클로저도 된다.
fn once[e: effects](
sc: TaskScope,
body: affine fn() effects e,
) effects {Supervisor.once} | e -> Stopped
}
+117
View File
@@ -0,0 +1,117 @@
// Process Supervisor — 죽는 일꾼을 감독한다.
//
// 앞의 넷은 자원 모델이었다. 여기는 **실패 위상**이다 — 자식이 죽었을 때
// 그 사실이 어디까지 가는가, 누가 멈출 수 있는가.
//
// 압박하려는 주장 셋:
// 1. spawn은 하나의 개념이고 실패가 어디까지 가느냐가 권한으로 갈린다
// 2. 재시작 가능한 자식은 copyable 클로저여야 한다 — affinity가 검사한다
// 3. 격리 경계는 recover가 아니다 — 죽었다는 사실만 넘어온다
import "cool.dev/std/list" as List
import "cool.dev/std/string" as String
import "cool.dev/std/int" as Int
import "../std-draft/sup" as Sup
import "../std-draft/db" as Db
// 로그를 쓸 권한. 감독자가 사람에게 보고하는 유일한 통로다 —
// 아래 "spawn은 값을 돌려주지 않는다" 참고.
pub capability Log {
fn write(s: String) effects {Log.write}
}
// ------------------------------------------------------------------
// 1. 재시작 가능한 일꾼
// ------------------------------------------------------------------
// 일감 하나를 처리한다. 자원을 **안에서** 얻고 안에서 반납한다.
//
// 이것이 재시작 가능성의 조건이다. 풀을 빌려 받을 뿐 lease를 들고 오지
// 않으므로, 이 함수를 감싼 클로저는 affine이 아니다. 죽어도 다음 번에
// 다시 획득하면 되고, 죽은 판의 lease는 감독자가 아니라 풀이 거둔다.
pub fn handle(pool: Db.Pool, log: Log, sql: String)
effects {Pool.acquire, Lease.query, Lease.release, Log.write} {
match pool.acquire() {
Err(_) => log.write("자리를 못 얻었다"),
Ok(l) => report(log, l, sql),
}
}
pub fn report(log: Log, own l: Db.Lease, sql: String)
effects {Lease.query, Lease.release, Log.write} {
let rows = l.query(sql)
let _ = l.release()
match rows {
Err(_) => log.write("질의가 실패했다"),
Ok(rs) => log.write(String.concat("행 ", Int.show(List.len(rs)))),
}
}
// 감독 아래 돌린다. body가 무표기 fn이므로 여러 번 돌 수 있다.
//
// pool과 log는 빌린 capability다. 빌린 값을 capture한 클로저는 use이고,
// use는 소유를 가져가는 자리로 못 간다 — supervise의 body는 빌림이므로 간다.
pub fn run_worker(sup: Sup.Supervisor, sc: TaskScope, pool: Db.Pool, log: Log, sql: String)
effects {Supervisor.supervise, Pool.acquire, Lease.query,
Lease.release, Log.write} -> Sup.Stopped {
sup.supervise(sc, fn() { handle(pool, log, sql) }, Sup.Policy { retries: 3 })
}
// ------------------------------------------------------------------
// 2. 재시작할 수 없는 일꾼
// ------------------------------------------------------------------
// lease를 밖에서 받아 들고 간다. 그래서 이 클로저는 affine이고,
// **supervise에 넘길 수 없다.** 한 번밖에 못 도는 것을 재시작할 수는 없다.
//
// 언어가 그 사실을 시그니처에서 막는다. 얼랭이 관례로 지키는 것을
// affinity가 검사한다 — 이 파일이 그것을 실제로 확인하는 자리다.
pub fn run_once(sup: Sup.Supervisor, sc: TaskScope, log: Log, own l: Db.Lease, sql: String)
effects {Supervisor.once, Lease.query, Lease.release, Log.write}
-> Sup.Stopped {
sup.once(sc, fn() { report(log, l, sql) })
}
// ------------------------------------------------------------------
// 3. 감독자가 보고한다
// ------------------------------------------------------------------
// 왜 멈췄는지 사람에게 알린다.
//
// Death에서 꺼낼 수 있는 것이 메시지뿐이다. 죽은 일꾼이 계산하던 값도,
// 들고 있던 자원도 여기 없다 — 격리 경계가 recover가 아니라는 것의
// 코드 수준 의미다. 담을 자리가 있으면 그것이 recover다.
pub fn announce(log: Log, s: Sup.Stopped) effects {Log.write} {
match s {
Sup.Done => log.write("끝났다"),
Sup.GaveUp(d) => match d {
Sup.Crashed(msg) => log.write(String.concat("포기: ", msg)),
},
}
}
// ------------------------------------------------------------------
// 4. 전부 엮기
// ------------------------------------------------------------------
// 일감마다 일꾼 하나. 블록을 나갈 때 전원이 join된다.
//
// ※ 발견: 여기서 각 일꾼이 어떻게 끝났는지 알 수 없다. spawn한 클로저의
// 반환값이 갈 데가 없기 때문이다. 그래서 보고를 값이 아니라 Log로 한다.
pub fn supervise_all(
sup: Sup.Supervisor,
root: TaskScope,
pool: Db.Pool,
log: Log,
jobs: List[String],
) effects {TaskScope.spawn, Supervisor.supervise, Pool.acquire,
Lease.query, Lease.release, Log.write} {
scope work = root {
List.each(jobs, fn(sql) {
work.spawn(fn() {
announce(log, run_worker(sup, work, pool, log, sql))
})
})
}
log.write("전원 종료")
}
+11 -3
View File
@@ -65,7 +65,14 @@ type expr =
| E_lit of lit * pos
| E_ident of string * pos
| E_list of expr list * pos
| E_struct of { name : string; fields : (string * expr) list; pos : pos }
(* modl은 가져온 모듈의 별칭이다. 이것이 없으면 다른 모듈의 struct를
만들 수 없다 — 타입은 쓸 수 있는데 값은 못 만드는 상태가 된다. *)
| E_struct of {
modl : string option;
name : string;
fields : (string * expr) list;
pos : pos;
}
| E_closure of closure
| E_if of { cond : expr; then_ : block; else_ : expr option; pos : pos }
| E_match of { scrutinee : expr; arms : arm list; pos : pos }
@@ -279,8 +286,9 @@ let rec buf_expr b = function
buf_expr b e)
xs;
Buffer.add_char b ')'
| E_struct { name; fields; _ } ->
Buffer.add_string b ("(struct " ^ name);
| E_struct { modl; name; fields; _ } ->
Buffer.add_string b
("(struct " ^ (match modl with Some a -> a ^ "." | None -> "") ^ name);
List.iter
(fun (n, e) ->
Buffer.add_string b (" (" ^ n ^ " ");
+3 -2
View File
@@ -190,8 +190,9 @@ let rec lower c (e : Ast.expr) : t =
| Ast.E_lit (l, _) -> I_lit l
| Ast.E_ident (n, pos) -> I_ref (classify c n, pos)
| Ast.E_list (xs, _) -> I_list (List.map (lower c) xs)
| Ast.E_struct { name; fields; _ } ->
I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields)
| Ast.E_struct { modl; name; fields; _ } ->
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
I_make (key, List.map (fun (n, e) -> (n, lower c e)) fields)
| Ast.E_closure cl ->
lpush c;
List.iter (fun (p : Ast.cl_param) -> lbind c p.cp_name) cl.cl_params;
+3 -2
View File
@@ -272,11 +272,12 @@ let rec walk st (ctx : ctx) (e : expr) : vinfo =
| E_list (xs, _) ->
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
| E_struct { name; fields; _ } ->
| E_struct { modl; name; fields; _ } ->
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
{
v_affine =
(match Hashtbl.find_opt st.aff name with
(match Hashtbl.find_opt st.aff key with
| Some b -> b
| None -> false);
v_use = false;
+13 -2
View File
@@ -412,11 +412,22 @@ and parse_primary st =
let parent = ident st "부모 scope 이름" in
let body = parse_block st in
E_scope { name; parent; body; pos = p }
(* name_or_struct<s>. struct를 만들 수 있는 문맥에서만 점을 미리 먹는다 —
Alias.Name { ... }의 "{"를 받을 자리가 여기밖에 없기 때문이다.
<no>에서는 안 먹으므로 if x.y { } 의 "{"는 언제나 블록이다. *)
| Token.Ident n ->
adv st;
if (not st.no_struct) && kind st = Token.LBrace then
if (not st.no_struct) && kind st = Token.Dot then begin
adv st;
let m = ident st "이름" in
if kind st = Token.LBrace then
let fields = parse_struct_lit_fields st in
E_struct { modl = Some n; name = m; fields; pos = p }
else E_field { obj = E_ident (n, p); name = m; pos = p }
end
else if (not st.no_struct) && kind st = Token.LBrace then
let fields = parse_struct_lit_fields st in
E_struct { name = n; fields; pos = p }
E_struct { modl = None; name = n; fields; pos = p }
else E_ident (n, p)
| _ -> err_expect st ""
+2 -2
View File
@@ -164,8 +164,8 @@ let rec resolve_expr st = function
(* 타입 이름은 그 타입에 딸린 함수의 이름공간이다: String.len *)
else external_ref st n pos
| E_list (xs, _) -> List.iter (resolve_expr st) xs
| E_struct { name; fields; pos } ->
resolve_ty st (T_named { modl = None; name; args = []; pos });
| E_struct { modl; name; fields; pos } ->
resolve_ty st (T_named { modl; name; args = []; pos });
List.iter (fun (_, e) -> resolve_expr st e) fields
| E_closure c ->
push st;
+83 -12
View File
@@ -197,7 +197,7 @@ let rec infer env (e : expr) : T.t =
| E_lit (L_int _, _) -> T.TInt
| E_lit (L_str _, _) -> T.TString
| E_lit (L_bool _, _) -> T.TBool
| E_ident (n, _) -> (
| E_ident (n, pos) -> (
match lookup env n with
| Some t -> t
| None -> (
@@ -215,9 +215,21 @@ let rec infer env (e : expr) : T.t =
| None -> (
match Hashtbl.find_opt env.ctors n with
| Some enum -> nullary_ctor env enum n
| None ->
env.saw_unknown <- true;
T.TUnknown)))))
| None -> (
(* 가져온 열거형의 생성자를 한정 없이 썼는가.
모른다고 넘기면 그 자리가 통째로 불투명해지고,
move 검사가 생성자인 줄 몰라 인자를 빌려 버린다 —
즉 affine 값이 복제된다 (보안 정리 ii). *)
match imported_ctor env n with
| Some full ->
err env pos
(Printf.sprintf
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오"
n full);
T.TUnknown
| None ->
env.saw_unknown <- true;
T.TUnknown))))))
| E_list (xs, pos) ->
let elem = T.fresh () in
List.iter
@@ -227,7 +239,9 @@ let rec infer env (e : expr) : T.t =
mismatch env pos elem t "리스트 원소의 타입이 서로 다릅니다")
xs;
T.TCon ("List", [ elem ])
| E_struct { name; fields; pos } -> infer_struct env name fields pos
| E_struct { modl; name; fields; pos } ->
let key = match modl with Some a -> a ^ "." ^ name | None -> name in
infer_struct env key fields pos
| E_closure c -> infer_closure env c None
| E_if { cond; then_; else_; pos } -> (
let c = infer env cond in
@@ -474,7 +488,17 @@ and infer_call env callee args pos =
| Some s ->
let params, eff, ret = instantiate s in
Some (T.TFn { affine = false; params; eff; ret })
| None -> None)))
| None -> (
(* 가져온 열거형의 생성자를 한정 없이 불렀는가.
모른다고 넘기면 move 검사가 생성자인 줄 몰라 인자를
빌리고, affine 값이 복제된다 (보안 정리 ii). *)
match imported_ctor env n with
| Some full ->
err env pos
(Printf.sprintf
"%s은(는) 가져온 열거형의 생성자입니다 — %s로 적으십시오" n full);
None
| None -> None))))
| _ -> (
match T.resolve (infer env callee) with
| T.TFn _ as t -> Some t
@@ -646,16 +670,63 @@ and check_pattern env (scrutinee : T.t) (p : pattern) =
| L_bool _ -> T.TBool
in
if not (T.unify scrutinee t) then mismatch env pos scrutinee t "패턴의 리터럴"
| P_bind (n, _) -> (
| P_bind (n, pos) -> (
match Hashtbl.find_opt env.ctors n with
| Some enum ->
check_ctor env scrutinee enum n [] Token.{ line = 0; col = 0 }
| None -> bind env n scrutinee)
| None -> (
match qualified_variant env scrutinee n with
| Some full -> variant_hint env pos n full
| None -> bind env n scrutinee))
| P_ctor { modl; name; args; pos } -> (
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
match Hashtbl.find_opt env.ctors name with
| Some enum -> check_ctor env scrutinee enum name args pos
| None -> List.iter (check_pattern env T.TUnknown) args)
let full = match modl with Some a -> a ^ "." ^ name | None -> name in
match Hashtbl.find_opt env.ctors full with
| Some enum -> check_ctor env scrutinee enum full args pos
| None -> (
match (modl, qualified_variant env scrutinee name) with
| None, Some q -> variant_hint env pos name q
| _ -> List.iter (check_pattern env T.TUnknown) args))
(* 한정되지 않은 이름이 match 대상 열거형의 variant를 가리키는가.
가져온 열거형만 본다 (variant 이름에 점이 있다) — 이 모듈의 열거형이면
env.ctors에서 이미 찾았을 것이기 때문이다.
이 진단이 없으면 인자 없는 variant를 한정 없이 적었을 때 그것이 바인딩이
되고, 그 팔이 전부를 잡아 **다음 팔이 도달 불가**라는 엉뚱한 자리에
오류가 난다. 틀린 줄을 가리키는 진단은 없느니만 못하다. *)
and qualified_variant env scrutinee n =
match T.resolve scrutinee with
| T.TCon (enum, _) -> (
match Hashtbl.find_opt env.enums enum with
| Some (_, variants) ->
List.find_opt
(fun (v, _) ->
match String.rindex_opt v '.' with
| Some i -> String.sub v (i + 1) (String.length v - i - 1) = n
| None -> false)
variants
|> Option.map fst
| None -> None)
| _ -> None
(* env.ctors에 "Alias.Name" 꼴로 들어 있는 생성자 중 이름이 n인 것.
이 모듈의 생성자면 위에서 이미 찾았으므로 여기 오지 않는다. *)
and imported_ctor env n =
Hashtbl.fold
(fun k _ acc ->
match acc with
| Some _ -> acc
| None -> (
match String.rindex_opt k '.' with
| Some i when String.sub k (i + 1) (String.length k - i - 1) = n ->
Some k
| _ -> None))
env.ctors None
and variant_hint env pos n full =
err env pos
(Printf.sprintf "%s은(는) 가져온 열거형의 variant입니다 — %s로 적으십시오 (한정 없는 이름은 바인딩입니다)"
n full)
and check_ctor env scrutinee enum name args pos =
match Hashtbl.find_opt env.enums enum with
+1 -1
View File
@@ -1,6 +1,6 @@
(test
(name test_coollang)
(libraries coollang)
(libraries coollang unix)
(deps
(glob_files %{workspace_root}/samples/*.cool)
(glob_files %{workspace_root}/std/*.cool)
+74
View File
@@ -1676,6 +1676,9 @@ let () =
( "../dogfoods/mini-shell/shell.cool",
[ "../dogfoods/std-draft/os.cool"; "../dogfoods/mini-shell/parse.cool" ]
);
("../dogfoods/std-draft/sup.cool", []);
( "../dogfoods/supervisor/workers.cool",
[ "../dogfoods/std-draft/sup.cool"; "../dogfoods/std-draft/db.cool" ] );
]
in
List.iter
@@ -2013,6 +2016,77 @@ let () =
\ }\n\
}"))
(* 가져온 모듈의 struct를 만들 수 없었다. 타입은 쓸 수 있는데 값은 못
만드는 상태였고, 개밥 넷이 전부 같은 모듈 안에서만 struct를 만들어
한 번도 안 걸렸다.
실제로 해소까지 되는지는 supervisor/workers.cool이 Sup.Policy를 만들면서
확인한다 (위 dogfoods 목록). 여기서는 문법만 고정한다. *)
let () =
let parses src = Result.is_ok (Parser.parse_result (Result.get_ok (Lexer.lex_result src))) in
check "Alias.Name { ... } 를 파서가 받는다"
(parses "fn f() -> P.P {\n P.P { n: 1 }\n}");
(* no_struct 문맥에서는 "{" 가 언제나 블록이다. 여기서 점을 먹으면
if a.b { } 의 블록이 struct 리터럴로 읽힌다 *)
check "if의 머리에서는 Alias.Name 뒤의 { 가 블록이다"
(parses "fn f(a: Int) -> Int {\n if A.b { 1 } else { 2 }\n}");
check "match의 머리에서도 그렇다"
(parses "fn f(a: Int) -> Int {\n match A.b {\n _ => 1,\n }\n}")
(* 가져온 열거형의 이름을 한정 없이 쓰면 조용히 통과했다.
- 패턴에서는 바인딩이 되어 그 팔이 전부를 잡고, 오류가 다음 팔에 났다
- 식에서는 모르는 이름이 되어 move 검사가 생성자인 줄 몰랐다.
그래서 affine 값을 두 번 감쌀 수 있었다 (보안 정리 ii)
Mini Shell에 다섯 군데 있었고 파이프 처리 전체가 검사되지 않고 있었다. *)
let () =
let dep =
"pub capability Fd {\n\
\ fn close(own self) effects {Fd.close}\n\
}\n\
pub enum Box { Wrap(Fd), Empty }\n"
in
let msgs src =
let dir = Filename.temp_file "cool" "" in
Sys.remove dir;
Unix.mkdir dir 0o755;
let w n c =
let oc = open_out (Filename.concat dir n) in
output_string oc c;
close_out oc
in
w "k.cool" dep;
w "j.cool" src;
let st = Session.create ~root:dir ~std:"../std" () in
Session.load st (Filename.concat dir "j.cool");
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
in
let has src frag =
List.exists
(fun m ->
let n = String.length frag in
let rec go i =
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
in
go 0)
(msgs src)
in
check "한정 없는 가져온 생성자를 식에서 거절한다"
(has
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n Wrap(x)\n}"
"K.Wrap로 적으십시오");
check "한정 없는 가져온 variant를 패턴에서 거절한다"
(has
"import \"./k\" as K\nfn f(b: K.Box) -> Int {\n\
\ match b {\n Empty => 0,\n K.Wrap(_) => 1,\n }\n}"
"K.Empty로 적으십시오");
check "한정하면 통과하고 이중 소비가 잡힌다"
(has
"import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n\
\ let a = K.Wrap(x)\n K.Wrap(x)\n}"
"이미 move되었습니다");
check "한정한 한 번은 통과한다"
(msgs "import \"./k\" as K\nfn f(own x: K.Fd) -> K.Box {\n K.Wrap(x)\n}" = [])
(* ================================================================== *)
(* 요약. 반드시 파일의 맨 끝이어야 한다. *)
(* *)