Library Imp_ko_utf8
Imp: 간단한 명령형 언어
Set Warnings "-notation-overridden,-parsing".
Require Import Coq.Bool.Bool.
Require Import Coq.Arith.Arith.
Require Import Coq.Arith.EqNat.
Require Import Coq.omega.Omega.
Require Import Coq.Lists.List.
Require Import Coq.omega.Omega.
Import ListNotations.
From LF Require Import Maps.
산술식과 부울식
Module AExp.
아래 두 정의는 산술식과 부울식의 _추상 구문_을 규정한다.
Inductive aexp : Type :=
| ANum : nat → aexp
| APlus : aexp → aexp → aexp
| AMinus : aexp → aexp → aexp
| AMult : aexp → aexp → aexp.
Inductive bexp : Type :=
| BTrue : bexp
| BFalse : bexp
| BEq : aexp → aexp → bexp
| BLe : aexp → aexp → bexp
| BNot : bexp → bexp
| BAnd : bexp → bexp → bexp.
이 장에서 프로그래머가 실제로 작성하는 실제 구문에서 이 추상 구문
트리로 변환하는 것을 생략할 것이다. 이 과정은 예를 들어 문자열
"1+2*3"을 다음 AST로
APlus (ANum 1) (AMult (ANum 2) (ANum 3)).
변환할 것이다.
선택 사항인 ImpParser장에서 이러한 변환을 할 수 있는 간단한 어휘
분석과 구문 분석 구현한다. 이 구현 방법을 이해학 위해서 해당 장을
이해할 필요는 _없다_. 하지만 이 기법을 다루는 강의를 듣지 않았다면
(예: 컴파일러 과목) 자세히 읽지 않아도 된다.
비교를 위해 여기 동일한 추상 구문을 정의하는
전통적인 BNF (Backus-Naur Form) 문법이 있다.
a ::= nat
| a + a
| a - a
| a * a
b ::= true
| false
| a = a
| a <= a
| not b
| b and b
위의 콕 버전과 비교하면...
두 가지 종류의 표기법, 즉 사람과 얘기할 때 비형식적인 표기법을,
구현하고 증명할 때 형식적인 표기법에 편해지는 것이 좋다.
- 이 BNF는 더 비형식적이다. 예를 들어 식의 표면적인 구문이 약간
남아 있다. 덧셈 연산은 +으로 작성하고 중위 기호라는 것을 알
수 있지만 어휘 분석과 구문 분석의 다른 면은 생략하고
있다. 예를 들어 +, -, ×의 상대적인 우선순위는 표현되어
있지 않다. 컴파일러를 구현할 때와 같이 이 구문을 형식적인
정의로 바꾸려면 추가적인 정보(와 인간 지능)이 필요할 수 있다.
- 반면에 BNF 버전은 더 간단하고 읽기 쉽다. 이 버전의 비형식적인
면은 표현이 유연하고, 일반적인 아이디어를 전달하는 것이 모든
상세한 내용을 정밀하게 정의하는 것 보다 중요한 칠판에서
토의와 같은 상황에서 큰 장점이 있다.
Fixpoint aeval (a : aexp) : nat :=
match a with
| ANum n ⇒ n
| APlus a1 a2 ⇒ (aeval a1) + (aeval a2)
| AMinus a1 a2 ⇒ (aeval a1) - (aeval a2)
| AMult a1 a2 ⇒ (aeval a1) × (aeval a2)
end.
Example test_aeval1:
aeval (APlus (ANum 2) (ANum 2)) = 4.
Proof. reflexivity. Qed.
비슷하게 부울식을 계산하면 부울값을 얻는다.
Fixpoint beval (b : bexp) : bool :=
match b with
| BTrue ⇒ true
| BFalse ⇒ false
| BEq a1 a2 ⇒ beq_nat (aeval a1) (aeval a2)
| BLe a1 a2 ⇒ leb (aeval a1) (aeval a2)
| BNot b1 ⇒ negb (beval b1)
| BAnd b1 b2 ⇒ andb (beval b1) (beval b2)
end.
최적화
Fixpoint optimize_0plus (a:aexp) : aexp :=
match a with
| ANum n ⇒
ANum n
| APlus (ANum 0) e2 ⇒
optimize_0plus e2
| APlus e1 e2 ⇒
APlus (optimize_0plus e1) (optimize_0plus e2)
| AMinus e1 e2 ⇒
AMinus (optimize_0plus e1) (optimize_0plus e2)
| AMult e1 e2 ⇒
AMult (optimize_0plus e1) (optimize_0plus e2)
end.
이 최적화 방법이 올바른지 몇가지 예제를 가지고 테스트해서 그
결과가 OK인지 볼 수 있다.
Example test_optimize_0plus:
optimize_0plus (APlus (ANum 2)
(APlus (ANum 0)
(APlus (ANum 0) (ANum 1))))
= APlus (ANum 2) (ANum 1).
Proof. reflexivity. Qed.
이 최적화가 정확한지 (즉, 최적화된 식을 계산하면 원래 식과 동일한
결과를 내는지)를 확신하고 싶으면 증명해야 한다.
Theorem optimize_0plus_sound: ∀ a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a. induction a.
- reflexivity.
- destruct a1.
+ destruct n.
× simpl. apply IHa2.
× simpl. rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
+
simpl. simpl in IHa1. rewrite IHa1.
rewrite IHa2. reflexivity.
-
simpl. rewrite IHa1. rewrite IHa2. reflexivity.
-
simpl. rewrite IHa1. rewrite IHa2. reflexivity. Qed.
콕 자동화
Theorem silly1 : ∀ ae, aeval ae = aeval ae.
Proof. try reflexivity. Qed.
Theorem silly2 : ∀ (P : Prop), P → P.
Proof.
intros P HP.
try reflexivity. apply HP. Qed.
위와 같은 모두 수동 증명인 경우에 try를 사용할 이유가 없지만
다음에 보게 되듯이, ; 고차원 전술과 함께 증명을 자동화하는 것이
매우 유용하다.
; 고차원 전술 (간단한 형태)
Lemma foo : ∀ n, leb 0 n = true.
Proof.
intros.
destruct n.
- simpl. reflexivity.
- simpl. reflexivity.
Qed.
이 증명을 ; 고차원 전술을 사용하여 간단하게 만들 수 있다.
Lemma foo' : ∀ n, leb 0 n = true.
Proof.
intros.
destruct n;
simpl;
reflexivity.
Qed.
try와 ;를 함께 사용하여 조금 전에 반복되는 패턴이 나타나
증명하기 귀찮았던 증명에서 이 반복 패턴을 제거할 수 있다.
Theorem optimize_0plus_sound': ∀ a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a.
induction a;
try (simpl; rewrite IHa1; rewrite IHa2; reflexivity).
- reflexivity.
-
destruct a1;
try (simpl; simpl in IHa1; rewrite IHa1;
rewrite IHa2; reflexivity).
+ destruct n;
simpl; rewrite IHa2; reflexivity. Qed.
콕 전문가는 induction같은 전술 다음에 많은 유사한 경우를 한번에
다 증명하기 위해 "...; try..."와 같은 관용 표현을 자주 사용한다.
자연스럽게 비형식적 증명에서도 이런 행위와 비슷한 것이 있다. 예를
들어, 최적화 정리에 대한 형식적 증명의 구조와 일치하는 비형식적
증명이 여기 있다.
_정리_: 모든 산술식 a에 대하여,
aeval (optimize_0plus a) = aeval a.
_증명_: a에 대한 귀납법에 의하여, 대부분의 경우 IH에 의해
증명한다. 남은 경우는 다음과 같다.
하지만 이 증명은 여전히 개선될 여지가 있다. (a = ANum n)인 첫번째
경우 매우 간단하다. 언급했던 IH를 따를 뿐인 경우들보다 훨씬 더
간단하다. 하지만 이 증명을 상세하게 작성하기로 결정했다. 아마도
처음부터 이러한 상세 증명을 버리고 "대부분의 경우 IH로부터 바로
또는 직접적으로 증명가능하다. 오직 흥미로운 경우는 APlus에 대한
경우이다..."고 언급만 한다면 더 좋고 명확한 증명이 될 것이다.
우리의 형식적 증명에서도 동일하게 개선할 수 있다. 이러한 내용을
여기에서 설명한다.
- 어떤 n에 대해 a = ANum n를 가정하자. 다음을 보여야 한다:
- 어떤 a1과 a2에 대해 a = APlus a1 a2를 가정하자. 다음을
보여야 한다:
Theorem optimize_0plus_sound'': ∀ a,
aeval (optimize_0plus a) = aeval a.
Proof.
intros a.
induction a;
try (simpl; rewrite IHa1; rewrite IHa2; reflexivity);
try reflexivity.
-
destruct a1; try (simpl; simpl in IHa1; rewrite IHa1;
rewrite IHa2; reflexivity).
+ destruct n;
simpl; rewrite IHa2; reflexivity. Qed.
; 고차원 전술 (일반적인 형태)
repeat 고차원 전술
Theorem In10 : In 10 [1;2;3;4;5;6;7;8;9;10].
Proof.
repeat (try (left; reflexivity); right).
Qed.
repeat T 전술은 결코 실패하지 않는다. 만일 전술 T가 원래
목적에 적용되지 않으면 repeat는 여전히 성공한 것으로 간주하되 원래
목적을 변경하지 않는다. 즉, 0번 반복한다.
Theorem In10' : In 10 [1;2;3;4;5;6;7;8;9;10].
Proof.
repeat (left; reflexivity).
repeat (right; try (left; reflexivity)).
Qed.
전술 repeat T도 T를 적용하는 횟수에 어떠한 상한선도 없다. 항상
성공하는 전술 T라면 영원 반복할 것이다. 예를 들어, simpl은
항상 성공하기 때문에 repeat simpl은 영원히 반복한다. 콕 언어
Gallina에서 모든 계산은 종료하는 것이 보장되는 반면 전술을 적용할
때의 계산은 그렇지 않다! repeat와 다른 전술이 하는 일은 콕으로
하여금 증명을 만드는 것을 가이드하는 것이기 때문에 전술의 비종료
가능성으로 콕의 논리적 일관성이 지켜지지 않는 영향을 주지 않는다.
증명을 만드는 과정이 무한히 끝나지 않으면 이것은 단지 증명을
만드는데 실패했다는 것을 뜻하지 잘못된 증명을 만들었다는 것은
아니다.
연습문제: 별 세 개 (optimize_0plus_b)
optimize_0plus 변환은 aexp 값을 변경하지 않기 때문에 bexp의 값을 변경하지 않고 bexp에 나타난 모든 aexp에 적용할 수 있어야 한다. bexp에 대한 변환을 실행하는 함수를 작성하고 이 변환이 건전함을 증명하시오. 우리가 살펴본 고차원 전술만 사용해서 이 증명을 최대한 우아하게 만들어보시오.Fixpoint optimize_0plus_b (b : bexp) : bexp
. Admitted.
Theorem optimize_0plus_b_sound : ∀ b,
beval (optimize_0plus_b b) = beval b.
Proof.
Admitted.
☐
☐
연습문제: 별 네 개, 선택사항 (optimizer)
_설계 연습문제_: optimize_0plus 함수로 구현한 최적화는 산술식과 부울식에 관한 많은 방법들 중 한가지일뿐이다. 더 복잡한 최적화를 작성하고 정확성을 증명해보시오. 작은 것부터 시작해서 (한가지 간단한 최적화를 추가해서 정확성을 증명하기) 점차 더 흥미로운 것을 쌓아보는 것이 가장 쉬울 것이다.새로운 전술 표기법을 정의하기
- 아래에 예시로 든 Tactic Notation 관용어는 여러 전술들을 하나의
명령어로 묶는 "축약된 전술"을 정의하는 쉬운 방법이다.
- 더 복잡한 프로그래밍을 위해서 콕은 Ltac이라 부르는 내장된
언어를 제공하고 이 언어에 포함된 기본 연산들로 증명 상태를
살펴보고 수정할 수 있다. 상세한 내용은 꽤 너무 복잡해서 여기에서
다룰 수 없다 (그리고 Ltac은 콕 설계에서 가장 아름다운 부분은
아니라고 일반적으로 생각한다!). 하지만 참고 매뉴얼과 콕에 관한
다른 책에서 LTac의 상세 내용을 확인할 수 있다. 콕 표준
라이브러리에 Ltac 정의의 많은 예가 있고 예로 사용할 수 있다.
- OCaml API도 있어서 낮은 수준에서 콕 내부 구조를 접근하는 전술을 만들때 사용할 수 있다. 하지만 일반 콕 사용자를 위해서는 이 어려움을 극복할만한 가치가 거의 없다.
Tactic Notation "simpl_and_try" tactic(c) :=
simpl;
try c.
simpl_and_try라 부르는 새로운 고차원 전술을 정의한다. 전술 c를
인자로 받고 simpl; try c 전술과 똑같이 동작하도록 정의한다.
이제 "simpl_and_try reflexivity."를 쓰면 증명에서 "simpl; try
reflexivity.]"처럼 동작할 것이다.
omega 전술
- 숫자 상수, 덧셈 (+와 S), 뺄셈 (-와 pred), 상수 곱셈 (이
것이 프레스버거 산술식이다),
- 등식 (=과 ≠)과 순서 (≤)와
- 논리 연결자 ∧, ∨, ¬, and →,
Example silly_presburger_example : ∀ m n o p,
m + n ≤ n + o ∧ o + 3 = p + 3 →
m ≤ p.
Proof.
intros. omega.
Qed.
(파일 시작 부분에 Require Import Coq.omega.Omega.를 보시오.
두세 가지 편리한 전술들
- clear H: 문맥에서 가정 H를 삭제.
- subst x: x = e나 e = x 가정을 문맥에서 찾아 문맥과 현재
목적에서 x를 e로 대체하고 가정을 지운다.
- subst: x = e나 e = x 형태의 _모든_ 가정을 대체한다.
- rename... into...: 증명 문맥에서 가정의 이름을 변경한다.
예를 들어 이 문맥에 변수 x가 있으면 rename x into y는 x가
나타난 곳에을 모두 y로 변경할 것이다.
- assumption: 목적과 정확히 매치되는 문맥에서 가정 H를 찾도록
시도한다. 만일 찾으면 apply H와 같이 동작한다.
- contradiction: False와 논리적으로 동치인 현재 문맥에서 가정
H를 찾으리려고 시도한다. 만일 그러한 가정을 찾으면 목적을
증명한다.
- constructor: 현재 목적을 증명하는데 적용할 수 있는 (현재 환경에서 어떤 Inductive 정의로부터) 생성자 c를 찾도록 시도하자. 만일 찾으면 apply c처럼 동작하시오.
관계로 계산을 정의하기
Module aevalR_first_try.
Inductive aevalR : aexp → nat → Prop :=
| E_ANum : ∀ (n: nat),
aevalR (ANum n) n
| E_APlus : ∀ (e1 e2: aexp) (n1 n2: nat),
aevalR e1 n1 →
aevalR e2 n2 →
aevalR (APlus e1 e2) (n1 + n2)
| E_AMinus: ∀ (e1 e2: aexp) (n1 n2: nat),
aevalR e1 n1 →
aevalR e2 n2 →
aevalR (AMinus e1 e2) (n1 - n2)
| E_AMult : ∀ (e1 e2: aexp) (n1 n2: nat),
aevalR e1 n1 →
aevalR e2 n2 →
aevalR (AMult e1 e2) (n1 × n2).
aevalR을 중위 표기법으로 표현하는 것은 편리할 것이다. e \\
n이라 작성하여 산술식 e를 값 n으로 계산하는 것을 의미한다.
Notation "e '\\' n"
:= (aevalR e n)
(at level 50, left associativity)
: type_scope.
End aevalR_first_try.
사실, 콕은 aevalR 자체의 정의에서 이 표기법을 사용하는 방법을
제공한다. e \\ n 형태의 문장을 포함하는 증명에 관하여 작업하고
있는 상황을 피해서 혼란을 줄이되, aevalR e n를 사용하여 작성한
정의를 참조해야 한다.
이 표기법을 먼저 "예약"하고 그 다음 표기법의 의미를 선언하여 정의한다.
Reserved Notation "e '\\' n" (at level 50, left associativity).
Inductive aevalR : aexp → nat → Prop :=
| E_ANum : ∀ (n:nat),
(ANum n) \\ n
| E_APlus : ∀ (e1 e2: aexp) (n1 n2 : nat),
(e1 \\ n1) → (e2 \\ n2) → (APlus e1 e2) \\ (n1 + n2)
| E_AMinus : ∀ (e1 e2: aexp) (n1 n2 : nat),
(e1 \\ n1) → (e2 \\ n2) → (AMinus e1 e2) \\ (n1 - n2)
| E_AMult : ∀ (e1 e2: aexp) (n1 n2 : nat),
(e1 \\ n1) → (e2 \\ n2) → (AMult e1 e2) \\ (n1 × n2)
where "e '\\' n" := (aevalR e n) : type_scope.
추론 규칙 표기법
(E_APlus) APlus e1 e2 \\ n1+n2
(E_ANum) ANum n \\ n
(E_APlus) APlus e1 e2 \\ n1+n2
(E_AMinus) AMinus e1 e2 \\ n1-n2
(E_AMult) AMult e1 e2 \\ n1*n2
Theorem aeval_iff_aevalR : ∀ a n,
(a \\ n) ↔ aeval a = n.
Proof.
split.
-
intros H.
induction H; simpl.
+
reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
+
rewrite IHaevalR1. rewrite IHaevalR2. reflexivity.
-
generalize dependent n.
induction a;
simpl; intros; subst.
+
apply E_ANum.
+
apply E_APlus.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
+
apply E_AMinus.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
+
apply E_AMult.
apply IHa1. reflexivity.
apply IHa2. reflexivity.
Qed.
고차원 전술을 더 사용하면 꽤 조금 더 짧게 증명할 수 있다.
Theorem aeval_iff_aevalR' : ∀ a n,
(a \\ n) ↔ aeval a = n.
Proof.
split.
-
intros H; induction H; subst; reflexivity.
-
generalize dependent n.
induction a; simpl; intros; subst; constructor;
try apply IHa1; try apply IHa2; reflexivity.
Qed.
Inductive bevalR: bexp → bool → Prop :=
.
Lemma beval_iff_bevalR : ∀ b bv,
bevalR b bv ↔ beval b = bv.
Proof.
Admitted.
☐
End AExp.
계산 기반 vs. 관계 기반 정의
Module aevalR_division.
예를 들어, 산술 연산을 확장해서 나눗셈 연산도 고려한다고 가정하자.
Inductive aexp : Type :=
| ANum : nat → aexp
| APlus : aexp → aexp → aexp
| AMinus : aexp → aexp → aexp
| AMult : aexp → aexp → aexp
| ADiv : aexp → aexp → aexp.
aeval 정의를 확장하여 이 새로운 연산을 처리하는 것은 간단하지
않을 수 있다 (ADiv (ANum 5) (ANum 0)의 결과는 무엇이어야
하는가?). 하지만 aevalR을 확장하는 것은 쉽다.
Reserved Notation "e '\\' n"
(at level 50, left associativity).
Inductive aevalR : aexp → nat → Prop :=
| E_ANum : ∀ (n:nat),
(ANum n) \\ n
| E_APlus : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (APlus a1 a2) \\ (n1 + n2)
| E_AMinus : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (AMinus a1 a2) \\ (n1 - n2)
| E_AMult : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (AMult a1 a2) \\ (n1 × n2)
| E_ADiv : ∀ (a1 a2: aexp) (n1 n2 n3: nat),
(a1 \\ n1) → (a2 \\ n2) → (n2 > 0) →
(mult n2 n3 = n1) → (ADiv a1 a2) \\ n3
where "a '\\' n" := (aevalR a n) : type_scope.
End aevalR_division.
Module aevalR_extended.
비결정적 숫자 생성 연산 any를 산술식에 추가하고 싶다고
가정하자. 이 연산을 계산하면 임의의 숫자를 만들어낸다. (모든
가능한 숫자들 중 _확률적_으로 선택하는 것과 동일하지 않음을
주목하시오. 결과에 대한 어떤 특정 분포를 규정하지 않지만 어떤
결과가 _가능한지_ 말하고 있을 뿐이다.)
Reserved Notation "e '\\' n" (at level 50, left associativity).
Inductive aexp : Type :=
| AAny : aexp
| ANum : nat → aexp
| APlus : aexp → aexp → aexp
| AMinus : aexp → aexp → aexp
| AMult : aexp → aexp → aexp.
다시 한 번 aeval을 확장하는 것은 약간 까다로울수 있다. 왜냐하면
계산은 식에서 숫자로 가는 결정적 함수가 _아니기_ 때문이다. 하지만
aevalR을 확장하는 것은 아무 문제가 없다.
Inductive aevalR : aexp → nat → Prop :=
| E_Any : ∀ (n:nat),
AAny \\ n
| E_ANum : ∀ (n:nat),
(ANum n) \\ n
| E_APlus : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (APlus a1 a2) \\ (n1 + n2)
| E_AMinus : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (AMinus a1 a2) \\ (n1 - n2)
| E_AMult : ∀ (a1 a2: aexp) (n1 n2 : nat),
(a1 \\ n1) → (a2 \\ n2) → (AMult a1 a2) \\ (n1 × n2)
where "a '\\' n" := (aevalR a n) : type_scope.
End aevalR_extended.
이 시점에서 의아해할 수 있다. 기본적으로 어떤 스타일을 사용해야
하는가? 위 예제는 관계형 정의가 근본적으로 함수형 정의보다 더
강력하다는 것을 보여준다. 함수로 표현하기 쉽지 않거나 정말로
함수가 _아닌_ 이와 같은 상황을 위하여 선택할 여지가 없다. 하지만
두 가지 스타일이 모두 가능한 경우는 어떠한가?
관계형 정의를 선호하는 한 가지는 더 우아하고 이해학 쉽다고
생각하기 때문이다. 다른 이유는 콕은 자동으로 Inductive
정의로부터 좋은 인버전과 귀납적 원리를 생성하기 때문이다.
반면에 함수형 정의가 종종 더 편리할 수 있다. 함수는 그 자체로
결정적이고 모든 인자에 대해 정의되어 있다. 관계형 정의에서는 이
성질들이 필요하면 명시적으로 증명해야 한다. 함수로 정의하면 콕의
계산 방법을 이용하여 증명동안 식을 간단하게 만들 수도 있다.
더우기 함수는 OCaml이나 Haskell로 실행가능한 코드를 직접 "추출"할
수 있다.
궁극적으로 이 선택은 특정 상황의 상세한 특징이나 단순히 취향에
달려있곤 하다. 정말로 대규모 콕 개발에서 함수와 관계 스타일
_둘다_ 정의하고 이 두 스타일이 일치함을 보이는 보조 정리를 두어
양쪽 관점에서 스위치하면서 증명하는 상황을 흔하게 본다.
변수를 포함하는 식
상태
Definition state := total_map nat.
Definition empty_state : state :=
t_empty 0.
Inductive aexp : Type :=
| ANum : nat → aexp
| AId : id → aexp
| APlus : aexp → aexp → aexp
| AMinus : aexp → aexp → aexp
| AMult : aexp → aexp → aexp.
두세 가지 변수 이름을 정의해서 축약된 표기법으로 사용한다. 나중에
예제를 쉽게 읽을 수 있도록 한다.
Definition W : id := Id "W".
Definition X : id := Id "X".
Definition Y : id := Id "Y".
Definition Z : id := Id "Z".
프로그램 변수 이름짓는 약속 (X, Y, Z은 타입을 대문자로
사용하던 것과 조금 충돌난다. Imp에 관한 장들에서 다형성을 많이
사용하지 않기 때문에 오버로딩으로 혼동스럽지 않을 것이다.)
bexp의 정의는 그대로 둔다 (새로운 aexp를 사용하는 것만
제외하고).
Inductive bexp : Type :=
| BTrue : bexp
| BFalse : bexp
| BEq : aexp → aexp → bexp
| BLe : aexp → aexp → bexp
| BNot : bexp → bexp
| BAnd : bexp → bexp → bexp.
Fixpoint aeval (st : state) (a : aexp) : nat :=
match a with
| ANum n ⇒ n
| AId x ⇒ st x
| APlus a1 a2 ⇒ (aeval st a1) + (aeval st a2)
| AMinus a1 a2 ⇒ (aeval st a1) - (aeval st a2)
| AMult a1 a2 ⇒ (aeval st a1) × (aeval st a2)
end.
Fixpoint beval (st : state) (b : bexp) : bool :=
match b with
| BTrue ⇒ true
| BFalse ⇒ false
| BEq a1 a2 ⇒ beq_nat (aeval st a1) (aeval st a2)
| BLe a1 a2 ⇒ leb (aeval st a1) (aeval st a2)
| BNot b1 ⇒ negb (beval st b1)
| BAnd b1 b2 ⇒ andb (beval st b1) (beval st b2)
end.
Example aexp1 :
aeval (t_update empty_state X 5)
(APlus (ANum 3) (AMult (AId X) (ANum 2)))
= 13.
Proof. reflexivity. Qed.
Example bexp1 :
beval (t_update empty_state X 5)
(BAnd BTrue (BNot (BLe (AId X) (ANum 4))))
= true.
Proof. reflexivity. Qed.
구문
Inductive com : Type :=
| CSkip : com
| CAss : id → aexp → com
| CSeq : com → com → com
| CIf : bexp → com → com → com
| CWhile : bexp → com → com.
대개 그러하듯이 두세 가지 Notation 선언을 사용하여 가독성을 높일
수 있다. 콕의 내장된 표기법과 충돌나지 않도록 가볍게 구문을
유지한다. 특히 이미 저의한 숫자 연산자와 부울 연산자와 혼동을
피하기 위해 aexps와 bexps에 대한 표기법은 도입하지 않는다.
Notation "'SKIP'" :=
CSkip.
Notation "x '::=' a" :=
(CAss x a) (at level 60).
Notation "c1 ;; c2" :=
(CSeq c1 c2) (at level 80, right associativity).
Notation "'WHILE' b 'DO' c 'END'" :=
(CWhile b c) (at level 80, right associativity).
Notation "'IFB' c1 'THEN' c2 'ELSE' c3 'FI'" :=
(CIf c1 c2 c3) (at level 80, right associativity).
예를 들어, 여기 다시 콕의 형식적인 정의로 표현한 팩토리얼 함수가
있다.
Definition fact_in_coq : com :=
Z ::= AId X;;
Y ::= ANum 1;;
WHILE BNot (BEq (AId Z) (ANum 0)) DO
Y ::= AMult (AId Y) (AId Z);;
Z ::= AMinus (AId Z) (ANum 1)
END.
Definition plus2 : com :=
X ::= (APlus (AId X) (ANum 2)).
Definition XtimesYinZ : com :=
Z ::= (AMult (AId X) (AId Y)).
Definition subtract_slowly_body : com :=
Z ::= AMinus (AId Z) (ANum 1) ;;
X ::= AMinus (AId X) (ANum 1).
Definition subtract_slowly : com :=
WHILE BNot (BEq (AId X) (ANum 0)) DO
subtract_slowly_body
END.
Definition subtract_3_from_5_slowly : com :=
X ::= ANum 3 ;;
Z ::= ANum 5 ;;
subtract_slowly.
Definition loop : com :=
WHILE BTrue DO
SKIP
END.
명령어를 실행하기
Fixpoint ceval_fun_no_while (st : state) (c : com)
: state :=
match c with
| SKIP ⇒
st
| x ::= a1 ⇒
t_update st x (aeval st a1)
| c1 ;; c2 ⇒
let st' := ceval_fun_no_while st c1 in
ceval_fun_no_while st' c2
| IFB b THEN c1 ELSE c2 FI ⇒
if (beval st b)
then ceval_fun_no_while st c1
else ceval_fun_no_while st c2
| WHILE b DO c END ⇒
st
end.
OCaml이나 Haskell과 같은 전통적인 함수형 프로그래밍 언어에서
다음과 같이 WHILE을 추가할 수도 있을 것이다.
Fixpoint ceval_fun (st : state) (c : com) : state := match c with
... | WHILE b DO c END => if (beval st b) then ceval_fun st (c;
WHILE b DO c END) else st end.
콕은 이런 정의를 받아들이지 않는다 ("에러: fix의 감소하는 인자를
추측할 수 없다"). 왜냐하면 정의하고자 하는 함수가 종료하지 않을 수
있기 때문이다. 정말로 항상 종료하는 것은 _아니다_. 예를 들어
ceval_fun 함수의 전체 버전을 위의 loop 프로그램에 적용하면
결코 종료하지 않을 것이다. 콕은 단지 함수형 프로그래밍 언어일 뿐만
아니라 일관성있는 논리이기 때문에 잠재적으로 종료하지 않는 함수는
제외할 필요가 있다. 만일 콕에서 비종료 재귀함수를 허용하면 잘못될
수 있다는 것을 보여주는 (무효!) 프로그램이 여기 있다.
Fixpoint loop_false (n : nat) : False := loop_false n.
즉 False와 같은 명제가 증명 가능할 수 있게 될 것이다
(loop_false 0는 False를 증명하는 것이다). 이러한 상황으로 콕의
논리적 일관성에 재앙이 발생할 것이다.
이렇게 ceval_fun의 모든 입력에 대해 종료하지 않기 때문에 콕으로
작성할 수 없다. 작성하려면 추가적인 트릭과 회피 방법이 필요하다
(이러한 방법이 무엇인지 궁금하면 ImpCEvalFun 장을 보시오).
관계로 계산하기
실행과정을 고려한 의미
(E_Skip) SKIP / st \\ st
(E_Ass) x := a1 / st \\ (t_update st x n)
(E_Seq) c1;;c2 / st \\ st''
(E_IfTrue) IF b1 THEN c1 ELSE c2 FI / st \\ st'
(E_IfFalse) IF b1 THEN c1 ELSE c2 FI / st \\ st'
(E_WhileFalse) WHILE b DO c END / st \\ st
(E_WhileTrue) WHILE b DO c END / st \\ st''
Reserved Notation "c1 '/' st '\\' st'"
(at level 40, st at level 39).
Inductive ceval : com → state → state → Prop :=
| E_Skip : ∀ st,
SKIP / st \\ st
| E_Ass : ∀ st a1 n x,
aeval st a1 = n →
(x ::= a1) / st \\ (t_update st x n)
| E_Seq : ∀ c1 c2 st st' st'',
c1 / st \\ st' →
c2 / st' \\ st'' →
(c1 ;; c2) / st \\ st''
| E_IfTrue : ∀ st st' b c1 c2,
beval st b = true →
c1 / st \\ st' →
(IFB b THEN c1 ELSE c2 FI) / st \\ st'
| E_IfFalse : ∀ st st' b c1 c2,
beval st b = false →
c2 / st \\ st' →
(IFB b THEN c1 ELSE c2 FI) / st \\ st'
| E_WhileFalse : ∀ b st c,
beval st b = false →
(WHILE b DO c END) / st \\ st
| E_WhileTrue : ∀ st st' st'' b c,
beval st b = true →
c / st \\ st' →
(WHILE b DO c END) / st' \\ st'' →
(WHILE b DO c END) / st \\ st''
where "c1 '/' st '\\' st'" := (ceval c1 st st').
함수 대신 관계로 계산을 정의하면 어떤 프로그램이 어떤 결과 상태로
계산되는 _증명_을 이제 만들 필요가 있다. 콕의 계산 방법을 그대로
활용할 수는 없다.
Example ceval_example1:
(X ::= ANum 2;;
IFB BLe (AId X) (ANum 1)
THEN Y ::= ANum 3
ELSE Z ::= ANum 4
FI)
/ empty_state
\\ (t_update (t_update empty_state X 2) Z 4).
Proof.
apply E_Seq with (t_update empty_state X 2).
-
apply E_Ass. reflexivity.
-
apply E_IfFalse.
reflexivity.
apply E_Ass. reflexivity. Qed.
Example ceval_example2:
(X ::= ANum 0;; Y ::= ANum 1;; Z ::= ANum 2) / empty_state \\
(t_update (t_update (t_update empty_state X 0) Y 1) Z 2).
Proof.
Admitted.
(X ::= ANum 0;; Y ::= ANum 1;; Z ::= ANum 2) / empty_state \\
(t_update (t_update (t_update empty_state X 0) Y 1) Z 2).
Proof.
Admitted.
☐
연습문제: 별 세 개, 고급 (pup_to_n)
1부터 X까지 숫자들을 합하는 1 + 2 + ... + X Imp 프로그램을 작성하시오. X = 2에 대해서 이 프로그램이 의도한 대로 실행한다고 증명하시오 (이 증명은 생각보다 더 까다롭다).Definition pup_to_n : com
. Admitted.
Theorem pup_to_2_ceval :
pup_to_n / (t_update empty_state X 2) \\
t_update (t_update (t_update (t_update (t_update (t_update empty_state
X 2) Y 0) Y 2) X 1) Y 3) X 0.
Proof.
Admitted.
☐
실행 결정성
Theorem ceval_deterministic: ∀ c st st1 st2,
c / st \\ st1 →
c / st \\ st2 →
st1 = st2.
Proof.
intros c st st1 st2 E1 E2.
generalize dependent st2.
induction E1;
intros st2 E2; inversion E2; subst.
- reflexivity.
- reflexivity.
-
assert (st' = st'0) as EQ1.
{ apply IHE1_1; assumption. }
subst st'0.
apply IHE1_2. assumption.
-
apply IHE1. assumption.
-
rewrite H in H5. inversion H5.
-
rewrite H in H5. inversion H5.
-
apply IHE1. assumption.
-
reflexivity.
-
rewrite H in H2. inversion H2.
-
rewrite H in H4. inversion H4.
-
assert (st' = st'0) as EQ1.
{ apply IHE1_1; assumption. }
subst st'0.
apply IHE1_2. assumption. Qed.
Imp 프로그램에 대한 추론
Theorem plus2_spec : ∀ st n st',
st X = n →
plus2 / st \\ st' →
st' X = n + 2.
Proof.
intros st n st' HX Heval.
Heval을 인버팅하여 ceval 계산의 한 단계를 확장한다. 이 경우
plus2는 할당문이므로 st'은 X의 새로운 값으로 확장한
st이어야 한다.
inversion Heval. subst. clear Heval. simpl.
apply t_update_eq. Qed.
Theorem loop_never_stops : ∀ st st',
~(loop / st \\ st').
Proof.
intros st st' contra. unfold loop in contra.
remember (WHILE BTrue DO SKIP END) as loopdef
eqn:Heqloopdef.
~(loop / st \\ st').
Proof.
intros st st' contra. unfold loop in contra.
remember (WHILE BTrue DO SKIP END) as loopdef
eqn:Heqloopdef.
loopdef가 종료함을 보이는 가정한 유도에 대한 귀납법에 따라
진행하시오. 대부분의 경우 즉시 모순이 발생한다 (그리고
inversion으로 진행한 한 단계에서 해결될 수 있을 것이다.)
Admitted.
Fixpoint no_whiles (c : com) : bool :=
match c with
| SKIP ⇒
true
| _ ::= _ ⇒
true
| c1 ;; c2 ⇒
andb (no_whiles c1) (no_whiles c2)
| IFB _ THEN ct ELSE cf FI ⇒
andb (no_whiles ct) (no_whiles cf)
| WHILE _ DO _ END ⇒
false
end.
이 명제는 while 반복문이 없는 프로그램의 경우에 대해서만 true를
낸다. Inductive를 사용하여 no_whilesR 성질을
작성하시오. no_whilesR c는 c에 while 반복문이 없는 프로그램일
때 증명가능하다는 성질이다. 그런 다음 no_whiles와 동치임을
증명하시오.
Inductive no_whilesR: com → Prop :=
.
Theorem no_whiles_eqv:
∀ c, no_whiles c = true ↔ no_whilesR c.
Proof.
Admitted.
☐
연습문제: 별 네 개 (no_whiles_terminating)
while 반복문을 포함하지 않는 Imp 프로그램은 항상 종료한다. 이에 관한 no_whiles_terminating 정리를 기술하고 증명하시오. no_whiles이나 no_whilesR 중 선호하는 것을 사용하시오.
☐
추가 연습문제
연습문제: 별 세 개 (stack_compiler)
HP 계산기, Forth와 Postscript 같은 프로그래밍 언어, Java 가상기계와 같은 추상 기계는 모두 스택을 사용하여 산술식을 계산한다. 예를 들어, 식+ 6 | 3 4 2 - * + 3, 6 | 4 2 - * + 4, 3, 6 | 2 - * + 2,
4, 3, 6] | - * + 2, 3, 6 | * + 6, 6 | + 12 |Inductive sinstr : Type :=
| SPush : nat → sinstr
| SLoad : id → sinstr
| SPlus : sinstr
| SMinus : sinstr
| SMult : sinstr.
스택 언어에서 프로그램을 계산하는 함수를 작성하시오. 입력으로
상태, 숫자 리스트로 표현한 스택, 명령어 리스트로 표현한 프로그램을
받아 프로그램 실행 후의 스택을 리턴해야 한다. 당신의 함수를 아래에
나열된 예제들을 가지고 테스트하시오.
SPlus, SMinus, SMult 명령어를 실행할 때 스택에 두 개 보다
적은 원소를 포함한다면 어떻게 해야할지 명령어 명세에 규정되어 있지
않음을 주목하라. 어떤 면에서는 그러한 상황에 무엇을 할 지 중요하지
않다. 왜냐하면 우리 컴파일러는 그러한 잘못 구성된 프로그램을 결코
만들지 않을 것이기 때문이다.
Fixpoint s_execute (st : state) (stack : list nat)
(prog : list sinstr)
: list nat
. Admitted.
Example s_execute1 :
s_execute empty_state []
[SPush 5; SPush 3; SPush 1; SMinus]
= [2; 5].
Admitted.
Example s_execute2 :
s_execute (t_update empty_state X 3) [3;4]
[SPush 4; SLoad X; SMult; SPlus]
= [15; 4].
Admitted.
다음으로 aexp를 스택 기계 프로그램으로 컴파일하는 함수를
작성하시오. 프로그램을 실행하는 효과는 그 식의 값을 스택에 올려
놓는 것과 동일해야 한다.
Fixpoint s_compile (e : aexp) : list sinstr
. Admitted.
s_compile을 정의한 다음 동작 여부를 테스트하기 위해 다음을
증명하시오.
Example s_compile1 :
s_compile (AMinus (AId X) (AMult (ANum 2) (AId Y)))
= [SLoad X; SPush 2; SLoad Y; SMult; SMinus].
Admitted.
☐
다음 정리를 증명하시오. 더 일반적인 보조 정리를 기술하여 사용할 수
있는 귀납적 가정을 얻을 필요가 있을 것이다. 주 정리는 이 보조
정리의 간단한 따름 정리가 될 것이다.
연습문제: 별 네 개, 고급 (stack_compiler_correct)
이전 연습문제에서 구현한 컴파일러의 정확성을 이제 증명할 것이다. SPlus, SMinus, SMult 명령어가 두 개 보다 적은 원소들을 포함하는 스택을 만나면 무엇을 할 지 명세에서 규정하고 있지 않음을 기억하시오 (정확성 증명을 더 쉽게 하려면 돌아가서 구현 방법을 변경하는 것이 필요할 수도 있다!).Theorem s_compile_correct : ∀ (st : state) (e : aexp),
s_execute st [] (s_compile e) = [ aeval st e ].
Proof.
Admitted.
☐
설명한 방식으로 BAnd의 단락 계산을 수행하도록 beval의 대체
버전을 작성하고, beval과 동치임을 증명하시오.
연습문제: 별 세 개, 선택 사항 (short_circuit)
대부분의 현대 프로그래밍 언어는 부울 and에 대한 "단락" 계산 규칙을 사용한다. BAnd b1 b2를 계산하려면 먼저 b1을 계산한다. 그 결과가 false이면 b2를 계산하지 않고 전체 BAnd 식이 즉시 false가 된다. 그렇지 않다면 b1를 계산해서 BAnd 식의 결과를 결정한다.
☐
Module BreakImp.
연습문제: 별 네 개, 고급 (break_imp)
Inductive com : Type :=
| CSkip : com
| CBreak : com
| CAss : id → aexp → com
| CSeq : com → com → com
| CIf : bexp → com → com → com
| CWhile : bexp → com → com.
Notation "'SKIP'" :=
CSkip.
Notation "'BREAK'" :=
CBreak.
Notation "x '::=' a" :=
(CAss x a) (at level 60).
Notation "c1 ;; c2" :=
(CSeq c1 c2) (at level 80, right associativity).
Notation "'WHILE' b 'DO' c 'END'" :=
(CWhile b c) (at level 80, right associativity).
Notation "'IFB' c1 'THEN' c2 'ELSE' c3 'FI'" :=
(CIf c1 c2 c3) (at level 80, right associativity).
그 다음 BREAK의 동작을 정의할 필요가 있다. 비형식적으로 일련의
명령어들 중 하나로 BREAK를 실행할 때 그 명령어들 실행을 중단하고
가장 안쪽 내포된 반복문이 끝나야 한다고 알린다. (만일 감싸는
반복문이 없으면 전체 프로그램이 종료한다.) 마지막 상태는 BREAK
문장이 실행되었을 때 상태와 동일해야 한다.
한 가지 중요한 점은 주어진 BREAK를 감싸는 여러 반복문들이 있을
때 어떻게 동작할 지 정의하는 것이다. 그러한 경우에 BREAK는 _가장
안쪽_ 반복문만 종료시킨다. 이렇게 다음을 실행한 다음...
X ::= 0;; Y ::= 1;; WHILE 0 <> Y DO WHILE TRUE DO BREAK END;; X
::= 1;; Y ::= Y - 1 END
... X의 값은 1이 되어야 하고 0이 아니다.
이러한 동작을 표현하는 한 가지 방법은, 계산 관계에 새로운
패러미터를 추가해서 다음 실행할 명령어가 BREAK 문장인지 규정하는
것이다.
Inductive result : Type :=
| SContinue : result
| SBreak : result.
Reserved Notation "c1 '/' st '\\' s '/' st'"
(at level 40, st, s at level 39).
직관적으로 c / st \\ s / st'는 만일 c를 상태 st에서 시작하면
st'에서 종료하고 맨 안쪽에 감싸는 반복문 (또는 전체 프로그램)은
즉시 빠져나가야 한다 (s = SBreak) 또는 이 실행은 정상적으로
이어가야 한다 (S = SContinue).
c / st \\ s / st' 관계의 정의는 정규 계산 관계 c / st \\ st'를
정의한 것과 매우 유사하다. 단지 종료할지 알리는 신호를 적절히
처리할 필요만 있을 뿐이다.
위에서 기술한 바에 따라 ceval 관계를 정의하시오.
- SKIP 명령어의 경우 상태는 바뀌지 않고 감싸고 있는 어떤
반복문도 정상적으로 실행을 계속한다.
- BREAK 명령어의 경우 상태는 바뀌지 않지만 SBreak 신호를
보낸다.
- 할당문의 경우 현재 상태의 변수 값을 문장에 따라 바꾸고
정상적으로 실행을 계속한다.
- IFB b THEN c1 ELSE c2 FI 형태의 명령어의 경우 상태는 Imp의
원래 의미에 따라 변경되도록 하되 어느 분기가 선택되었는지
무관하게 그 분기를 실행하면서 발생한 신호를 전파한다.
- c1 ;; c2 형태의 명령어의 경우 c1을 먼저 실행한다. 만일
SBreak 신호가 발생하면 c2를 실행하지 않고 SBreak 신호를
감싸는 문맥에 전파한다. 결과 상태는 c1을 실행하면서 얻는
상태와 동일하다. 만일 c1 실행에서 SBreak 신호가 발생하지
않으면 c1을 실행해서 얻은 상태에서 c2를 실행하고 거기에서
발생한 신호가 있다면 전파한다.
- 마지막으로 WHILE b DO c END 형태의 반복문의 의미는 이전과 거의 동일하다. 유일하게 다른 점은 b가 true일 때 c를 계산하고 c에서 신호를 발생했는지 검사하는 것이다. 신호가 SContinue이면 원래 의미대로 실행을 진행하간다. 그렇지 않으면 반복문 실행을 멈추고 결과 상태를 현재 반복의 실행에서 나온 상태와 동일하다. 어느 경우든 BREAK는 가장 안쪽의 반복문만을 종료시키기 때문에 WHILE은 SContinue 신호를 낸다.
Inductive ceval : com → state → result → state → Prop :=
| E_Skip : ∀ st,
CSkip / st \\ SContinue / st
where "c1 '/' st '\\' s '/' st'" := (ceval c1 st s st').
이제 ceval 정의 다음 성질들을 증명하시오.
Theorem break_ignore : ∀ c st st' s,
(BREAK;; c) / st \\ s / st' →
st = st'.
Proof.
Admitted.
Theorem while_continue : ∀ b c st st' s,
(WHILE b DO c END) / st \\ s / st' →
s = SContinue.
Proof.
Admitted.
Theorem while_stops_on_break : ∀ b c st st',
beval st b = true →
c / st \\ SBreak / st' →
(WHILE b DO c END) / st \\ SContinue / st'.
Proof.
Admitted.
Theorem while_break_true : ∀ b c st st',
(WHILE b DO c END) / st \\ SContinue / st' →
beval st' b = true →
∃ st'', c / st'' \\ SBreak / st'.
Proof.
Admitted.
(WHILE b DO c END) / st \\ SContinue / st' →
beval st' b = true →
∃ st'', c / st'' \\ SBreak / st'.
Proof.
Admitted.
Theorem ceval_deterministic: ∀ (c:com) st st1 st2 s1 s2,
c / st \\ s1 / st1 →
c / st \\ s2 / st2 →
st1 = st2 ∧ s1 = s2.
Proof.
Admitted.
c / st \\ s1 / st1 →
c / st \\ s2 / st2 →
st1 = st2 ∧ s1 = s2.
Proof.
Admitted.
☐
End BreakImp.
연습문제: 별 네 개, 선택 사항 (add_for_loop)
C 스타일 for 반복문을 명령어 언어에 추가하고, for 반복문의 의미를 정의하도록 ceval 정의를 수정하고, for 반복문에 필요한 경우들을 추가해서 이 파일에 있는 모든 증명을 여전히 콕이 받아드리도록 작성하시오.
☐