Lowent 매뉴얼←↑→

38 왜 증명하는가

먼저 알아야 할 것

31장 짓고 시험하기 · 시험·두 백엔드 대조·계약 대조
14장 계약 · 강제되는 계약은 사실이 되어 검사를 지운다

돌아보기

31장에서 두 백엔드 대조 같은 검사는 결함의 무엇을 보이고 무엇은 보이지 못한다고 했는가?

답. 결함이 있음은 보이지만 없음은 보이지 못한다고 했다. 두 구현이 같은 답을 낸다고 둘 다 맞는 것은 아니기 때문이다. 그리고 없음은 증명의 몫이라고 했다. 제10부가 그 증명을 다룬다.

이 장의 필요성과 맥락

제10부는 이 언어가 스스로에 대해 하는 주장이 어디까지 참인지를 다룬다. 앞의 장들에서 “이것은 증명되어 있다” 는 말이 여러 번 나왔다 — 넓히기, mod 의 색인 안전, 차용 규칙, 병렬의 결정성. 이 부는 그 말들이 각각 무엇을 뜻하고, 어떤 모델에 대한 것이며, 어디서 끝나는지를 정직하게 모은다. 그 첫 장은 왜 증명이 필요한지, 그리고 증명·전수 검사·대조라는 세 층이 어떻게 겹치는지를 세운다.

이 장이 끝나면

시험이 “있음(∃)” 을, 증명이 “없음(∀)” 을 보인다는 구별을 예제로 확인한다. 증명·전수 검사·대조 세 층이 각각 무엇을 잡고 무엇을 놓치는지, 이 저장소의 증명이 쓰는 세 전략(불변식 보존·순서 구조로 환원·전수 검사)을 알게 된다. 증명된 것의 실측 규모와, 증명이 컴파일러가 무엇을 거절해야 하는지를 정한다는 관계도 이해하게 된다.

이 장에서 답할 질문

  1. 증명이 컴파일러 코드와 어떻게 이어지는가? 정리가 있다고 컴파일러가 달라지는가?

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
Qed481
Admitted · Axiom0
증명 스크립트 줄 수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 이 부를 읽는 약속#

이 부는 세 가지를 지킨다.

그리고 이 언어가 기대는 생각을 여섯 줄로 줄이면 이렇다. 여섯 모두 수학으로 뒷받침되거나, 뒷받침되지 않는다고 적혀 있다 — 그 구별이 이 부의 목적이다.

  1. 틀린 프로그램은 쓸 수 없게 만든다. 틀린 것을 잡는 것보다 표현할 수 없게 하는 쪽이 싸다(42장).
  2. 모르는 것은 모른다고 답한다. 실패는 option·result 라는 값이지 숨은 예외가 아니다.
  3. 비용은 보여야 한다. 할당·곁효과·복사가 모두 소스에 적힌다(44장).
  4. 의심스러우면 검사를 남긴다. 검사를 지우는 것은 증명이 있을 때뿐이다(41장).
  5. 두 구현이 같은 답을 내야 한다. VM 과 네이티브가 다르면 컴파일러 결함이다(31장).
  6. 못 한 것은 적는다. 가정·한계·미증명을 남긴다(50장).

문. 증명이 컴파일러 코드와 어떻게 이어지는가? 정리가 있다고 컴파일러가 달라지는가?

답. 정리는 컴파일러가 무엇을 거절해야 하는지를 정한다. 넓히기 표에서 uN ⊑ iM 의 조건이 N < M(엄격)이라는 것, 루프를 넘어 사는 쓰기 빌림과 본문의 소유자 쓰기가 만나는 모양 하나만 막으면 충분하다는 것이 정리에서 나왔다. 반대 방향도 있다. 구현이 겪은 결함(“차용을 함수로 세탁하기”)을 정리로 옮겨 두면, 누가 그 검사를 지우는 순간 증명이 깨진다. 이야기는 잊히지만 정리는 남는다(42장).

 정리 ─────────────▶ 컴파일러가 거절할 모양을 정한다
                     예: uN ⊑ iM 의 조건은 N < M (엄격)
 구현이 겪은 결함 ─▶ 정리로 옮긴다
                     예: 차용을 함수로 세탁하기
                     그 검사를 지우면 증명이 깨진다

복습 정리

시험은 틀리는 입력이 있음을 보이고, 증명은 모든 입력에서 틀리지 않음을 보인다. Lowent 는 증명·전수 검사·대조를 겹쳐 쓰고, “증명됨”· “전수 검사됨”·“스케치” 를 섞지 않는다. 31 개 Coq 파일에 Admitted·Axiom 이 없다. 증명은 불변식 보존·순서 구조 환원·전수 검사로 이루어지며, 컴파일러가 무엇을 거절해야 하는지를 정한다. 증명은 언어의 규칙에 대한 것이지 당신의 프로그램이 옳다는 보증이 아니다.