diff --git a/docs/thesis.md b/docs/thesis.md index 627ee9c..8fdefc3 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -347,14 +347,44 @@ panic(message) -> Never. 키워드다 (prelude가 없어 함수로 두면 매번 그게 없으면 panic은 식 자리에서 못 쓴다. - 언어 수준 recover가 없다. 붙잡는 것이 있으면 그것은 예외이고, 예외는 시그니처에 안 적히므로 철학 1과 충돌한다. - 런타임은 격리 경계를 가질 수 있다 — 테스트 러너가 첫 사례이고, 서버가 - 두 번째가 될 것이다. 죽은 것을 되살리는 게 아니라 죽었음을 관찰한다. - 되감기를 하지 않는다. ※ panic 시 자원 해제 여부는 자원 모델과 함께 결정한다 — 지금은 열어둔다. - 0으로 나누기, assert 실패, 미래의 범위·오버플로가 전부 이 하나로 모인다. ※ panic은 프로그램의 버그를 말한다. 예상되는 실패는 Result다. 호출자가 대처할 수 있는 것을 panic으로 처리하면 오용이다. +■ panic은 어디까지 올라가는가 (실패의 단위) +콜 스택의 root가 아니라 scope 트리의 root다. 즉 실패의 단위는 프로세스가 +아니라 태스크다. scope가 이미 그 모양이기 때문이다 — 렉시컬이고, 블록을 +나가는 것이 join이며, 자식이 죽었다는 사실이 부모에게 도달하는 지점이 +문법에 이미 있다(닫는 중괄호). + + panic은 scope 트리를 타고 올라간다. 자식이 panic하면 그 scope는 join + 지점에서 panic한다. root까지 도달하면 실행이 끝난다. + +v0는 태스크가 하나뿐이라 이 규칙이 관측되지 않는다 — "태스크가 죽는다"와 +"프로그램이 죽는다"가 같은 사건이다. 그래도 지금 적는 이유는, 동시성이 +진짜가 될 때 이 결정을 새로 하면 이미 쓰인 코드의 의미가 바뀌기 때문이다. + +전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가 +바뀔 뿐, scope가 실패한다는 사실은 바뀌지 않는다. +※ 형제 취소는 여전히 미정이다. 대가를 알고 미룬다 — 취소가 없으면 형제 + 하나가 끝나지 않을 때 죽은 자식의 panic이 join에 도달하지 못한다. + 실패가 hang에 가려진다. 이것이 취소가 필요한 이유이기도 하다. + +■ 격리 경계는 recover가 아니다 +런타임은 격리 경계를 가질 수 있다. 테스트 러너가 첫 사례이고, 서버의 +요청 경계가 두 번째가 될 것이다. 구분선은 이것이다: + + 실패한 계산이 만든 값은 경계를 넘지 못한다. + 경계가 얻는 것은 "죽었다"는 사실과 메시지뿐이다. + +recover는 같은 스택에서 재개하고 지역 변수에 접근한다 — 금지다. +격리 경계는 죽었음을 관찰할 뿐이다 — 허용이다. Erlang의 supervisor가 죽은 +프로세스의 상태를 물려받지 않는 것과 같다. +테스트 러너가 이 원칙대로다: 실패한 테스트에서 아무 값도 가져오지 않고 +이름과 메시지만 얻는다. 서버 경계도 언어 기능이 아니라 scope 위에 얹는다. + ■ 내장 테스트 test "이름" { ... }. 파라미터가 없으므로 capability를 받을 수 없고, capability를 만드는 문법도 없다. 따라서 effect-free임이 증명된다 — diff --git a/test/test_coollang.ml b/test/test_coollang.ml index d6aa01c..ae5ca2f 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -1624,3 +1624,32 @@ let () = check "samples/app의 테스트가 돈다" (List.length rs >= 4); check "samples/app의 테스트가 전부 통과한다" (List.for_all (fun (r : Interp.test_result) -> r.t_failure = None) rs) + +(* panic은 scope 트리를 타고 올라간다. 자식이 panic하면 그 scope는 실패하고, + scope 뒤의 코드는 실행되지 않는다. + + v0는 태스크가 하나라 이 규칙이 축약된 형태로만 관측된다. 아래에서 "자식 + 셋"이 안 도는 것은 취소가 아니라 순차 실행의 부산물이다 — 취소는 아직 + 정하지 않았다. 고정하는 것은 전파이지 형제의 운명이 아니다. *) +let () = + match + run_src + (console + ^ "pub fn main(c: Console, root: TaskScope)\n\ + \ effects {Console.print, TaskScope.spawn} {\n\ + \ c.print(\"scope 앞\")\n\ + \ scope work = root {\n\ + \ work.spawn(fn() { c.print(\"자식 하나\") })\n\ + \ work.spawn(fn() { panic(\"자식 둘이 죽었다\") })\n\ + \ }\n\ + \ c.print(\"scope 뒤\")\n\ + }") + with + | out, Some e -> + check "자식의 panic이 scope 밖으로 전파된다" (has_sub e.message "자식 둘이 죽었다"); + check "panic 전의 출력은 남는다" (has_sub out "자식 하나"); + check "scope 뒤는 실행되지 않는다" (not (has_sub out "scope 뒤")) + | _, None -> + check "자식의 panic이 scope 밖으로 전파된다" false; + check "panic 전의 출력은 남는다" false; + check "scope 뒤는 실행되지 않는다" false