Lowent 매뉴얼←↑→

50 증명하지 않은 것

먼저 알아야 할 것

38장 왜 증명하는가 · 증명됨·전수 검사됨·스케치를 섞지 않는다
31장 짓고 시험하기 · 컴파일러를 맞대어 검증하는 다섯 방법
48장 문법과 해시의 증명 · 앞단은 가장 약한 이음매다

돌아보기

38장은 “증명된 언어로 짠 프로그램은 옳다” 가 왜 오개념이라고 했는가?

답. 증명은 언어의 규칙에 대한 것이지 당신의 프로그램에 대한 것이 아니기 때문이다. 차용 규칙이 건전하다는 정리는 번역을 통과한 프로그램에 차용 위반이 없다는 것만 말한다. 이 장은 그 경계를 끝까지 따라가, 증명이 기대는 것과 덮지 않는 것을 한곳에 모은다.

이 장의 필요성과 맥락

증명을 자랑하는 문서는 대개 두 가지 가운데 하나를 한다 — 경계를 적지 않거나, 맨 앞에 한 줄로 면피하고 잊는다. 둘 다 읽는 사람을 잘못된 확신으로 데려간다. 증명의 값은 그것이 무엇을 덮지 않는지 아는 데서 나온다. 이 장이 책의 마지막인 것은 편집상의 우연이 아니다. 앞의 장들이 무엇을 주장했는지 알고 나서야 그 경계가 뜻을 갖는다. 이 장을 읽지 않고 이 책을 인용하면, 이 책이 말하지 않은 것을 말한 것으로 만들게 된다.

이 장이 끝나면

증명이 기대는 신뢰 기반(Coq 커널·C 컴파일러·BLAKE3·빌린 증명 따위)과 그것이 틀렸을 때 무너지는 범위를 알게 된다. 모델과 구현 사이의 간극을 잇는 장치들이 경험적인지 정적인지 가르고, 가장 약한 이음매가 앞단이라는 것을 이해한다. 주제별로 남은 것, 이 언어를 “전면 정적 안전” 이라 부르면 안 되는 이유, 그리고 결국 무엇을 믿어도 되는지를 정리한다.

이 장에서 답할 질문

  1. 그렇다면 이 모든 증명은 결국 무엇을 사는가?

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 과 네이티브를 비교하지만 둘은 같은 앞단을 지난다. 앞단이 틀리면 둘 다 똑같이 틀린다. 파서를 문법 모델과 직접 대조하려던 시도는 모델이 실제 문법보다 작아서 결론을 낼 만큼의 짝을 얻지 못했다. 이 자리를 닫으려면 앞단을 독립적으로 한 벌 더 구현해 대조해야 하고, 그 비용(두 벌을 유지하는 것)은 아직 치르지 않았다.

실제 사례. 이 책이 찾은 어긋남들

이 책의 예제는 모두 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장). 모두 그 장에 경고로 적었고 개발 저장소에 결함으로 올렸다. ✔ 는 “이것이 막혀야 한다” 는 근거이고, 막혔는지는 그 장의 경고와 결함 목록을 함께 본다.

복습 정리

증명은 Coq 커널·C 컴파일러·BLAKE3·빌린 증명·운영체제 같은 신뢰 기반에 기댄다. 가장 큰 간극은 모델과 구현 사이이고, 전수 검사·대조·자기 고발은 경험적으로, 증명서 검산은 정적으로 그 사이를 잇는다. 가장 약한 이음매는 앞단이다. 반복의 종료·부동소수·effect-row·파서의 모델 대응은 증명 밖이다. 이 언어는 정적 검사와 실행 검사를 섞은 설계이고, 증명은 주로 순차 단편에 대한 것이다. 믿어도 되는 것과 경험적인 것과 다루지 않은 것을 갈라서 읽는다.