docs: 실패의 단위를 정한다 — panic은 scope 트리를 타고 올라간다
"panic이 unrecoverable한 건 프로세스까지인가"라는 질문에 답이 없었다. 답이 없었던 이유는 동시성 모델을 안 정했기 때문이고, 두 질문이 사실 하나다. 정한 것: 실패의 단위는 프로세스가 아니라 태스크다. panic은 콜 스택의 root가 아니라 scope 트리의 root까지 올라간다. scope가 이미 그 모양이기 때문이다 — 렉시컬이고, 블록을 나가는 것이 join이며, 자식이 죽었다는 사실이 부모에게 도달하는 지점이 문법에 이미 있다. 전파는 취소를 정하지 않고도 정의된다. 취소가 생기면 형제들이 언제 멈추는지가 바뀔 뿐 scope가 실패한다는 사실은 안 바뀐다. 그래서 지금 적어도 v1의 취소 설계를 앞당겨 닫지 않는다. 여전히 미정인 것과 그 대가도 적었다: 취소가 없으면 형제 하나가 끝나지 않을 때 죽은 자식의 panic이 join에 도달하지 못한다. 실패가 hang에 가려진다. 그리고 "격리 경계는 recover가 아니다"의 구분선을 명시했다 — 실패한 계산이 만든 값은 경계를 넘지 못하고, 경계가 얻는 것은 죽었다는 사실과 메시지뿐이다. 테스트 러너가 이미 그 원칙대로 돈다. v0는 태스크가 하나라 규칙이 축약된 형태로만 관측되지만, 그 형태로 테스트에 고정했다. 테스트 주석에 "자식 셋이 안 도는 것은 취소가 아니라 순차 실행의 부산물"이라고 적어 뒀다 — 나중에 이걸 취소로 오해하지 않도록. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
+32
-2
@@ -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임이 증명된다 —
|
||||
|
||||
Reference in New Issue
Block a user