38 왜 증명하는가
먼저 알아야 할 것
돌아보기
31장에서 두 백엔드 대조 같은 검사는 결함의 무엇을 보이고 무엇은 보이지 못한다고 했는가?
답. 결함이 있음은 보이지만 없음은 보이지 못한다고 했다. 두 구현이 같은 답을 낸다고 둘 다 맞는 것은 아니기 때문이다. 그리고 없음은 증명의 몫이라고 했다. 제10부가 그 증명을 다룬다.
이 장의 필요성과 맥락
mod 의 색인 안전, 차용 규칙, 병렬의 결정성. 이 부는 그 말들이 각각 무엇을 뜻하고, 어떤 모델에 대한 것이며, 어디서 끝나는지를 정직하게 모은다. 그 첫 장은 왜 증명이 필요한지, 그리고 증명·전수 검사·대조라는 세 층이 어떻게 겹치는지를 세운다.이 장이 끝나면
이 장에서 답할 질문
- 증명이 컴파일러 코드와 어떻게 이어지는가? 정리가 있다고 컴파일러가 달라지는가?
38.1 시험은 있음을, 증명은 없음을#
examples/ch38/average.low
module average .
rem run: mean2 10 20
rem trap: mean2 4294967295 1
rem test
fn mean2 input a u32 . input b u32 . output u32 .
do
return div (add a b) 2 .
end
test small_numbers
do
expect eq (mean2 10 20) 15 .
expect eq (mean2 0 0) 0 .
expect eq (mean2 1000000 3000000) 2000000 .
end
실행 결과
$ lowentc --run mean2 average.low 10 20
mean2(10, 20) = 15
$ lowentc --run mean2 average.low 4294967295 1
== ir diagnostics (1) ==
0:0 E-VM-OVERFLOW: integer overflow at the declared width (use wrap_*/sat_*, or prove the range)
$ lowentc --test average.low
[PASS] small_numbers
== tests: 1 run, 1 passed, 0 FAILED ==
두 수의 평균을 구하는 mean2 는 시험 세 개를 모두 통과한다. 그런데 4294967295 와 1 을 주면 add a b 가 u32 에서 넘쳐 멈춘다. 시험을 백만 개 더 써도 저자가 큰 수를 고르지 않으면 이 결함은 드러나지 않는다.
이것이 시험의 본성이다. 시험은 “이 입력에서 틀린다(∃)” 를 보일 수 있지만 “어떤 입력에서도 틀리지 않는다(∀)” 를 보일 수 없다. 자연수를 백만 개 확인해도 모든 자연수를 확인한 것이 아니다. ∃ 를 아무리 모아도 ∀ 가 되지 않는다. 이 간극이 증명이 필요한 유일한 이유다.
mean2 의 입력: u32 두 개 = 약 1.8 × 10^19 쌍
시험 (10,20) (0,0) (10^6,3·10^6) 세 점을 찍었다 → 셋 다 맞다
반례 (4294967295, 1) 찍지 않은 자리 → 넘쳐 멈춘다
증명 ─── 모든 쌍 ─────────── 한 번에 모두에 대해 말한다Lowent 에서 이 결함은 적어도 조용하지는 않다. C 였다면 넘친 합이 감겨 틀린 평균을 냈을 것이다. 여기서는 멈춘다(4장). 그리고 고친 판은 넘칠 수 없는 모양으로 적는다.
examples/ch38/average_fixed.low
module average_fixed .
rem run: mean2 4294967295 1
rem ir
fn mean2 input a u32 . input b u32 . output u32 .
do
let lo u32 be min a b .
let hi u32 be max a b .
return add lo (div (sub hi lo) 2) .
end
실행 결과
$ lowentc --run mean2 average_fixed.low 4294967295 1
mean2(4294967295, 1) = 2147483648
$ lowentc --ir average_fixed.low
-- runtime checks (interval analysis: overflow · division · narrowing) --
1 / 3 removed (33%)
작은 쪽에 두 수 차이의 절반을 더하면 넘칠 수 없다. --ir 은 세 검사 가운데 하나를 지웠다고 말한다. 남은 두 검사가 실제로 넘칠 수 없는데도 남는 것은 구간 분석이 min·max 의 관계를 모르기 때문이다. 분석은 안전한 방향으로만 틀린다 — 증명하지 못하면 검사를 남긴다.
38.2 세 층을 겹친다#
| 층 | 무엇을 하나 | 얼마나 센가 | 무엇을 놓치나 |
|---|---|---|---|
| 증명 | 모든 경우를 논리로 | 모델 안에서 반례가 존재할 수 없다 | 모델이 틀리면 무력하다 |
| 전수 검사 | 정해진 크기의 모든 경우를 실제로 | 그 크기 안에서 확실하다 | 큰 입력을 못 본다 |
| 대조 | 두 구현을 같은 입력으로 맞대 본다 | 불일치를 잡는다 | 둘이 똑같이 틀리면 못 잡는다 |
표 38.1 — 검증의 세 층
셋이 다른 종류의 실수를 잡기 때문에 겹쳐 쓴다. 누가 무엇을 맞대는지 그리면 이렇다.
명세의 규칙 ── 옮긴다 ──▶ Coq 모델 ── 증명 ──▶ 모델 안의 모든 경우
▲
│ 전수 검사: 작은 크기에서 모델과 구현을 맞댄다
▼
컴파일러 ─┬─ VM 으로 돌린다 ──┐
└─ C 로 내보낸다 ───┴─▶ 대조: 두 답이 다르면 컴파일러 결함증명은 모델을, 대조는 구현을 보고, 전수 검사는 그 둘 사이를 잇는다. 이 저장소에서는 증명이 못 잡은 결함을 대조가 잡았고, 대조가 못 잡은 결함을 증명이 잡았다. 그리고 이 부는 세 낱말을 섞지 않는다. “증명됨” 은 Coq 이 모든 경우를 확인했다는 뜻이고, “전수 검사됨” 은 정해진 크기의 모든 경우를 돌렸다는 뜻이며, “스케치” 는 사람이 논증했을 뿐 기계가 확인하지 않았다는 뜻이다.
흔한 오해. 증명된 언어로 짠 프로그램은 옳다
mean2 는 차용 위반도 없고 넘침도 조용하지 않지만, 여전히 틀린 설계였다. 프로그램이 옳은지는 계약·시험·검토가 답한다.38.3 무엇이 증명되어 있나#
증명 파일은 모두 docs/proofs/coq/ 에 있다. 이 판에서 센 규모다.
| 항목 | 수 |
|---|---|
| Coq 파일 | 31(검사기 추출 파일 1 포함) |
| 정리(Theorem) · 따름정리(Corollary) · 보조정리(Lemma) | 179 · 15 · 250 |
| 예(Example) | 33 |
Qed | 481 |
Admitted · Axiom | 0 |
| 증명 스크립트 줄 수 | 10,350 |
| 검증 도구 | Rocq 9.2(전부) · Coq 8.20.1(약한 메모리 둘을 뺀 나머지) |
표 38.2 — 기계 검증된 증명의 규모
Coq 에서 Admitted 는 “이건 참이라고 치고 넘어간다” 이고 Axiom 은 “이건 가정한다” 다. 그런 자리가 하나라도 있으면 그 파일의 모든 정리가 그 가정에 기댄다. 둘 다 0 이다. 무엇을 증명했고 무엇을 아직 안 했는지는 docs/proofs/LEDGER.md 에 적혀 있다.
묶음으로 보면 이렇다.
| 묶음 | 한 문장으로 | 장 |
|---|---|---|
| 도구 | 뒤의 장이 쓰는 수학 여섯 가지 — 부분 순서·격자·고정점·귀납·추상해석 | 39장 |
| 수 | 값이 조용히 변하는 넓히기는 없고, 나눗셈이 실패하는 경우는 정확히 알려져 있다 | 40장 |
| 경계 | 검사를 지운 자리는 범위 안임이 증명된 자리다 | 41장 |
| 소유와 차용 | 번역을 통과한 프로그램은 실행 중 차용 위반이 없다 | 42장 |
| 되풀이 | 고정점에 닿는 반복은 몇 번을 돌든 차용 위반이 없다 | 43장 |
| 효과 | 효과 선언은 실제로 일어나는 효과를 덮고, 그 위에서 최적화가 적법하다 | 44장 |
| 경합과 병렬 | 안전 코드에는 데이터 경합이 없고, 병렬 결과는 순차와 비트까지 같다 | 45장 |
| 약한 메모리 | 기본 기억 차례면 순서대로 생각해도 되고, 약하게 적으면 거동이 늘어난다 | 46장 |
| 락 | CAS 스핀락은 상호배제를 주고, 오름차순 잠금은 교착하지 않는다 | 47장 |
| 문법 | 한 문장은 한 문장짜리 블록이고, 어느 닫개로 닫아도 같은 나무가 나온다 | 48장 |
| 해시 | 주석은 해시를 바꾸지 않고, 인코딩이 같으면 인터페이스가 같다 | 49장 |
표 38.3 — 증명된 것의 묶음과 이 부의 장
마지막 장(50장)은 이 모두가 기대는 것과 덮지 않는 것을 모은다.
38.4 증명하는 세 가지 방법#
증명 스크립트를 읽을 필요는 없다. 그러나 어떤 방식으로 증명했는지 알면 그 정리를 얼마나 믿어야 하는지 감이 온다.
불변식 보존. “프로그램이 한 걸음 갈 때마다 어떤 성질이 유지된다” 를 보인다. 시작에서 참이고(기초) 한 걸음이 참을 참으로 넘기면(보존), 몇 걸음을 가도 참이다. 수학적 귀납법의 프로그램 판이다. 차용 규칙과 반복의 정리가 이 방식이다.
순서 구조로 환원. 문제를 어떤 순서에 관한 문제로 바꾼 뒤 순서의 성질로 답한다. “u8 은 u32 에 안전하게 들어간다” 를 부분 순서로 만들면, “두 타입을 합칠 때 무엇을 골라야 하나” 가 최소 상계라는 잘 알려진 개념이 된다. 수의 넓히기와 약한 메모리 모델이 이 방식이다.
전수 검사. 아직 모든 크기에 대해 증명하지 못한 성질은 크기를 정해 그 안의 모든 경우를 돌린다. 이것은 증명이 아니고, 이 부는 그렇게 적는다.
38.5 이 부를 읽는 약속#
이 부는 세 가지를 지킨다.
- 낱말을 섞지 않는다. “증명됨”·“전수 검사됨”·“스케치” 는 서로 다른 세기다. “이 언어는 증명됐다” 가 사실은 “짧은 경우들을 돌려 봤다” 인 경우가 흔하다.
- 정리마다 막는 결함이 붙는다. 무엇을 막는지 적을 수 없는 정리는 싣지 않았다. 읽는 사람에게 값이 없는 정리는 여기서 다룰 까닭이 없다.
- 이 부는 규범이 아니다. 언어의 정의는 명세의 조항 정본이다. 이 부와 명세가 어긋나면 이 부가 틀렸다.
그리고 이 언어가 기대는 생각을 여섯 줄로 줄이면 이렇다. 여섯 모두 수학으로 뒷받침되거나, 뒷받침되지 않는다고 적혀 있다 — 그 구별이 이 부의 목적이다.
- 틀린 프로그램은 쓸 수 없게 만든다. 틀린 것을 잡는 것보다 표현할 수 없게 하는 쪽이 싸다(42장).
- 모르는 것은 모른다고 답한다. 실패는
option·result라는 값이지 숨은 예외가 아니다. - 비용은 보여야 한다. 할당·곁효과·복사가 모두 소스에 적힌다(44장).
- 의심스러우면 검사를 남긴다. 검사를 지우는 것은 증명이 있을 때뿐이다(41장).
- 두 구현이 같은 답을 내야 한다. VM 과 네이티브가 다르면 컴파일러 결함이다(31장).
- 못 한 것은 적는다. 가정·한계·미증명을 남긴다(50장).
문. 증명이 컴파일러 코드와 어떻게 이어지는가? 정리가 있다고 컴파일러가 달라지는가?
답. 정리는 컴파일러가 무엇을 거절해야 하는지를 정한다. 넓히기 표에서 uN ⊑ iM 의 조건이 N < M(엄격)이라는 것, 루프를 넘어 사는 쓰기 빌림과 본문의 소유자 쓰기가 만나는 모양 하나만 막으면 충분하다는 것이 정리에서 나왔다. 반대 방향도 있다. 구현이 겪은 결함(“차용을 함수로 세탁하기”)을 정리로 옮겨 두면, 누가 그 검사를 지우는 순간 증명이 깨진다. 이야기는 잊히지만 정리는 남는다(42장).
정리 ─────────────▶ 컴파일러가 거절할 모양을 정한다
예: uN ⊑ iM 의 조건은 N < M (엄격)
구현이 겪은 결함 ─▶ 정리로 옮긴다
예: 차용을 함수로 세탁하기
그 검사를 지우면 증명이 깨진다복습 정리
Admitted·Axiom 이 없다. 증명은 불변식 보존·순서 구조 환원·전수 검사로 이루어지며, 컴파일러가 무엇을 거절해야 하는지를 정한다. 증명은 언어의 규칙에 대한 것이지 당신의 프로그램이 옳다는 보증이 아니다.