50 증명하지 않은 것
먼저 알아야 할 것
돌아보기
38장은 “증명된 언어로 짠 프로그램은 옳다” 가 왜 오개념이라고 했는가?
답. 증명은 언어의 규칙에 대한 것이지 당신의 프로그램에 대한 것이 아니기 때문이다. 차용 규칙이 건전하다는 정리는 번역을 통과한 프로그램에 차용 위반이 없다는 것만 말한다. 이 장은 그 경계를 끝까지 따라가, 증명이 기대는 것과 덮지 않는 것을 한곳에 모은다.
이 장의 필요성과 맥락
이 장이 끝나면
이 장에서 답할 질문
- 그렇다면 이 모든 증명은 결국 무엇을 사는가?
50.1 신뢰 기반 — 증명이 기대는 것#
| 믿는 것 | 왜 믿나 | 틀리면 |
|---|---|---|
| Coq·Rocq 의 커널 | 작은 핵이고, 두 판(8.20·9.2)이 같은 답을 낸다 | 모든 정리가 무의미해진다 |
| Iris | 락 증명에만 필요하다 | 락 정리만 무너진다 |
iRC11 · gpfsl · Iris 의 rw_spin_lock | 약한 메모리·SPSC·rwlock 읽기 쪽의 증명을 빌렸다 | 그 정리들만 무너진다 |
| C 컴파일러(gcc·clang) | 대안이 없다 | 네이티브 코드가 중간 표현과 달라진다 |
| BLAKE3 의 충돌 저항 | 내용 주소화의 전제다 | 캐시가 다른 내용을 같다고 본다 |
| 운영체제·하드웨어 | 대안이 없다 | 전부 |
proven_c_lib(기반 라이브러리) | 자체 시험이 있다 | 자료구조가 조용히 틀린다 |
| 모델이 언어를 옳게 담았다 | 사람이 읽어서 확인한다 | 아래의 간극 |
표 50.1 — 신뢰 기반
50.2 가장 큰 간극 — 모델과 구현 사이#
정리는 모델에 대한 것이다.
모델은 사람이 쓴 것이다.
컴파일러는 다른 언어로 쓴 다른 코드다.
그 둘이 같다는 것은 증명되지 않았다.이음매를 잇는 장치들이다.
| 장치 | 무엇을 하나 | 성격 |
|---|---|---|
| 유계 전수 모델 검사 | 작은 크기의 모든 경우에서 모델의 판정과 구현의 판정이 같은지 | 경험적 |
| 실제 프로그램에서 뽑은 사건 | 진짜 프로그램의 차용 사건을 같은 모델에 먹인다 | 경험적 · 실제 프로그램 |
| 두 백엔드 대조 | VM 과 네이티브가 같은 답을 내는지 | 경험적 |
분석 자기 고발(E-VM-ANALYSIS) | 컴파일러의 “지워도 된다” 를 실행으로 반박한다 | 경험적 |
| 증명서 검산 | 지운 검사의 근거를 독립 검산기가 산술로 다시 따진다 | 정적 |
| 회귀 시험 | 출력이 바뀌면 보인다 | 회귀만 |
표 50.2 — 모델과 구현을 잇는 장치
앞의 넷은 경험적이다. “지금 출력이 옳다” 가 아니라 “이 입력들에서 어긋나지 않았다” 를 말한다. 증명서 검산은 성격이 다르다. 그 자리를 때리는 입력이 없어도 말한다. 다만 그것도 사실을 믿고 추론만 검사한다. 두 층은 서로 다른 것을 잡는다. 증명이 못 잡은 결함을 대조가 잡았고, 대조가 못 잡은 결함을 증명이 잡았다. 겹쳐 쓰는 이유다.
가장 약한 이음매는 앞단(어휘·파서)이다. 두 백엔드 대조는 VM 과 네이티브를 비교하지만 둘은 같은 앞단을 지난다. 앞단이 틀리면 둘 다 똑같이 틀린다. 파서를 문법 모델과 직접 대조하려던 시도는 모델이 실제 문법보다 작아서 결론을 낼 만큼의 짝을 얻지 못했다. 이 자리를 닫으려면 앞단을 독립적으로 한 벌 더 구현해 대조해야 하고, 그 비용(두 벌을 유지하는 것)은 아직 치르지 않았다.
실제 사례. 이 책이 찾은 어긋남들
method 로 부른 op 의 효과가 부르는 쪽에 번지지 않는 것(23장), 식이 든 ensures 가 경고 없이 검사되지 않는 것(14장), arg 의 번호가 두 백엔드에서 다른 것(2장), expr 섬에서 비교와 and 의 우선순위가 명세표와 다른 것(8장). 모두 증명의 모델이 아니라 구현과 문서 쪽의 어긋남이다. 예제를 실제로 돌려 싣는다는 규율이 없었다면 이 책은 그 어긋남을 사실처럼 적었을 것이다.50.3 남은 것 — 주제별로#
| 무엇 | 상태 |
|---|---|
| 효과 체계 | 증명됨. task_group 이 concurrent 를 흡수하는 규칙과 효과 닫힘은 모델 밖 |
| 효과 기반 최적화 | 증명됨(재정렬·공통 부분식 제거·기억·죽은 코드 제거). 호출 경계와 멈추는 시점은 밖 |
| 반복의 종료 | 다루지 않는다. 무한 반복은 정당한 프로그램이다 |
| 타입 체계 전체의 건전성 | 부분. effect-row 는 없고 증명도 없다 |
| 정규화의 완전성 | 부분. 효과는 집합으로 정규화(증명됨). 계약 절은 적힌 차례에 민감하고 그것이 안전한 방향임을 증명했다 |
| 덩어리 해시의 다구현 호환 | 미확정. 명세 수준의 약속 |
| 파서 ≡ 문법 모델 | 미증명 |
표 50.3 — 언어 코어와 도구
| 무엇 | 상태 |
|---|---|
| RC11 일반 약한 차례 | 부분. 모델 안의 메타정리는 증명됨. 특정 락 없는 알고리즘을 약한 차례로 검증한 것은 SPSC 하나(빌린 증명) |
| 교착 자유 | 증명됨. 단 잠금 오름차순 규율은 사람이 지켜야 한다(도구가 강제하지 않는다) |
| 굶주림 없음 | 증명됨. 라운드로빈·양보를 가정하고, 우선순위와 차단은 밖 |
| level 1 병렬의 구현 | 구현됨. 정리는 구현을 검증하지 않는다 |
| 락 없는 자료구조 | SPSC 링 버퍼 하나. MPSC·MPMC·seqlock 은 빌릴 증명이 없어 넣지 않았다 |
| 원자 차례 → 기계어 | C11 atomic 으로 1:1 방출하므로 나머지는 C 컴파일러(신뢰 기반)의 몫. 매핑 자체는 시험으로 확인 |
표 50.4 — 동시성
| 무엇 | 상태 |
|---|---|
| 부동소수의 성질(반올림·NaN·−0) | 다루지 않았다 |
| 구간 분석의 부동소수 지원 | 없다 |
page_fault·blocking 따위 효과 원자 | 어휘만 있고 원시 연산이 없다 |
| effect-row 다형성 | 없다 |
표 50.5 — 기능 범위
examples/ch50/floats.low
module float_gap .
rem run: tenths
rem run: close_enough
fn tenths output bool .
do
return eq (add 0.1 0.2) 0.3 .
end
fn close_enough output bool .
do
let d f64 be sub (add 0.1 0.2) 0.3 .
return lt (abs d) 0.000000001 .
end
실행 결과
$ lowentc --run tenths floats.low
tenths() = 0
$ lowentc --run close_enough floats.low
close_enough() = 1
0.1 과 0.2 를 더한 값은 0.3 과 같지 않다. 이것은 결함이 아니라 IEEE 754 의 성질이고, 그 성질은 이 언어의 증명이 다루지 않았다. 부동소수의 같음은 close_enough 처럼 허용 오차로 묻는다. 부동소수가 걸린 계산에서 이 책의 “증명됨” 은 아무것도 보증하지 않는다.
50.4 “전면 정적 안전” 으로 팔지 않는다#
정적으로 막는 것 차용 · 영역 · 참조 탈출 · 병렬 겹침 · 정수 넓히기
실행이 막는 것 증명 안 된 자리의 경계 검사 · 넘침 · 계약 검사
세대 핸들의 늘어진 참조이 언어는 섞어 쓰는 설계다. 그리고 그것을 정적이라고 부르지 않는다. 비교를 정직하게 적으면, Rust 는 RustBelt 로 증명이 있고, Zig 는 안전을 주장하지 않는다(그것도 정직한 태도다). Lowent 는 “증명 있음 — 주로 순차 단편에 대해” 다. 그 조건절을 떼면 거짓말이 된다. 그리고 release_fast 빌드 모드는 남은 계약 검사를 없애 신뢰 경계를 하나 더 연다(14장). “미정의 동작이 없다” 는 말은 안전한 부분집합 안에서만 참이다.
문. 그렇다면 이 모든 증명은 결국 무엇을 사는가?
답. 두 가지를 산다. 첫째, 컴파일러가 무엇을 거절해야 하는지가 감이 아니라 논증으로 정해진다. 규칙이 하나로 충분한지, 조건이 ≤ 인지 < 인지가 증명에서 나온다. 둘째, 구현이 겪은 결함이 정리로 남는다. 이야기는 잊히지만, 누가 그 검사를 지우면 증명이 깨진다. 증명은 프로그램이 옳다고 보증하지 않지만, 언어가 조용히 틀리는 자리를 체계적으로 줄인다.
50.5 그래서 무엇을 믿어도 되나#
✔ 정수가 조용히 값을 바꾸는 일은 없다 (수 · 증명됨)
✔ 번역을 통과한 코드에 차용 위반은 없다 (소유 · 증명됨, 순차)
✔ 효과 선언이 실제로 일어나는 효과를 덮는다 (효과 · 증명됨, 모델 안)
✔ 안전 코드에 데이터 경합은 없다 (동시성 · 증명됨)
✔ 병렬 결과는 순차와 비트까지 같다 (병렬 · 증명됨)
✔ 기본 차례면 순서대로 사고해도 된다 (약한 메모리 · 증명됨, ∀실행)
✔ 경계 검사를 지운 자리는 범위 안임이 논증된 자리 (경계 · 증명됨 + 검산)
△ 컴파일러가 이 모델을 정확히 따라간다 (경험적)
△ 약한 차례를 명시한 코드 (SPSC 하나만 증명 — 나머지는 감사 대상)
✘ 부동소수의 세부 (다루지 않았다)마지막 줄들이 있는 문서를 믿어도 된다. 없는 문서는 다시 봐야 한다.
흔한 오해. ✔ 가 붙은 줄은 이 판의 컴파일러가 언제나 그렇게 한다는 뜻이다
reduce 의 누산 시작값이 항등원이 아니면 나누어 돈 네이티브 답이 순차와 다르고(27장), let 이름을 mut_ref 로 빌려주면 불변이 뚫리며 (12장), 권한 자리에 수를 넘기면 권한 없는 op 이 출력한다(16장). 모두 그 장에 경고로 적었고 개발 저장소에 결함으로 올렸다. ✔ 는 “이것이 막혀야 한다” 는 근거이고, 막혔는지는 그 장의 경고와 결함 목록을 함께 본다.복습 정리