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:
+13
-1
@@ -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 ;
|
||||
|
||||
@@ -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
@@ -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, 자원 정리 | **완료** |
|
||||
|
||||
@@ -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))),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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 ^ " ");
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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)
|
||||
|
||||
@@ -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}" = [])
|
||||
|
||||
(* ================================================================== *)
|
||||
(* 요약. 반드시 파일의 맨 끝이어야 한다. *)
|
||||
(* *)
|
||||
|
||||
Reference in New Issue
Block a user