exhaust: match exhaustiveness와 도달 불가 팔 검사
철학 1이 나열한 다섯 항목 중 비어 있던 자리를 채운다. Maranget의 usefulness 알고리즘으로 반례를 만들어 "빠진 경우"를 이름으로 말한다 — 중첩된 자리의 반례도 찾는다(Some(Rect(_, _))). 이 검사가 왜 지금 필요한가: interface hash가 enum 정의 본문을 입력으로 삼는 이유가 바로 이것이다. upstream에 variant가 하나 늘면 downstream의 match가 깨져야 하는데, 검사가 없으면 깨질 것이 없다. 다음 마일스톤(모듈 경계를 넘는 재검사)의 핵심 시나리오가 여기에 걸려 있다. 테스트로 그 시나리오를 직접 고정했다 — 같은 코드가 variant 둘일 때는 통과하고 셋이 되면 깨진다. 구현 중 한 번 틀렸다. 리터럴 패턴을 와일드카드로 줄였더니 Int 리터럴 두 개로 match가 완전해져 버렸다. 리터럴은 인자 없는 생성자이고, 타입의 생성자 집합이 무한하므로 리터럴만으로는 결코 완전해지지 않는다. 생성자 집합을 알 수 없는 타입(외부 타입, 미지수)은 검사하지 않는다. 모르는 것을 위반이라고 말하지 않는다. definite init은 문법이 이미 보장한다는 것을 문서에 적었다 — let이 항상 초기화식을 요구하므로 별도 검사가 필요 없다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
+99
-3
@@ -583,7 +583,8 @@ let () =
|
||||
f <> "05_move_errors.cool"
|
||||
&& f <> "08_syntax_errors.cool"
|
||||
&& f <> "09_type_errors.cool"
|
||||
&& f <> "10_effect_errors.cool")
|
||||
&& f <> "10_effect_errors.cool"
|
||||
&& f <> "11_exhaustiveness.cool")
|
||||
|> List.sort compare
|
||||
in
|
||||
List.iter
|
||||
@@ -603,9 +604,12 @@ let () =
|
||||
(match Driver.typecheck (Filename.concat dir "10_effect_errors.cool") with
|
||||
| Ok () -> check "10은 effect 오류를 내야 한다" false
|
||||
| Error errors -> check "10의 effect 오류" (List.length errors >= 6));
|
||||
match Driver.typecheck (Filename.concat dir "05_move_errors.cool") with
|
||||
(match Driver.typecheck (Filename.concat dir "05_move_errors.cool") with
|
||||
| Ok () -> check "05는 move 오류를 내야 한다" false
|
||||
| Error errors -> check "05의 move 오류" (List.length errors >= 9)
|
||||
| Error errors -> check "05의 move 오류" (List.length errors >= 9));
|
||||
match Driver.typecheck (Filename.concat dir "11_exhaustiveness.cool") with
|
||||
| Ok () -> check "11은 exhaustiveness 오류를 내야 한다" false
|
||||
| Error errors -> check "11의 exhaustiveness 오류" (List.length errors >= 7)
|
||||
|
||||
(* ================================================================== *)
|
||||
(* effect / capability 검사 *)
|
||||
@@ -817,3 +821,95 @@ let () =
|
||||
|
||||
let () =
|
||||
check "모르는 타입은 copyable로 본다" (move_ok "fn f(x: Widget) -> Widget {\n x\n}")
|
||||
|
||||
(* ================================================================== *)
|
||||
(* exhaustiveness *)
|
||||
(* ================================================================== *)
|
||||
|
||||
let e3 = "enum E {\n A(Int),\n B,\n C,\n}\n"
|
||||
|
||||
let () =
|
||||
check "모든 variant를 덮으면 통과"
|
||||
(type_ok
|
||||
(e3
|
||||
^ "fn f(x: E) -> Int {\n\
|
||||
\ match x {\n\
|
||||
\ A(n) => n,\n\
|
||||
\ B => 1,\n\
|
||||
\ C => 2,\n\
|
||||
\ }\n\
|
||||
}"));
|
||||
check "빠진 variant를 이름으로 말한다"
|
||||
(type_has
|
||||
(e3
|
||||
^ "fn f(x: E) -> Int {\n match x {\n A(n) => n,\n B => 1,\n }\n}"
|
||||
)
|
||||
"빠진 경우: C");
|
||||
check "와일드카드가 나머지를 덮는다"
|
||||
(type_ok
|
||||
(e3
|
||||
^ "fn f(x: E) -> Int {\n match x {\n A(n) => n,\n _ => 0,\n }\n}"
|
||||
));
|
||||
check "Bool의 생성자 집합도 유한하다"
|
||||
(type_has "fn f(b: Bool) -> Int {\n match b {\n true => 1,\n }\n}"
|
||||
"빠진 경우: false");
|
||||
check "Option"
|
||||
(type_has
|
||||
"fn f(o: Option[Int]) -> Int {\n match o {\n Some(n) => n,\n }\n}"
|
||||
"빠진 경우: None");
|
||||
check "Result"
|
||||
(type_ok
|
||||
"fn f(r: Result[Int, Int]) -> Int {\n\
|
||||
\ match r {\n\
|
||||
\ Ok(n) => n,\n\
|
||||
\ Err(e) => e,\n\
|
||||
\ }\n\
|
||||
}");
|
||||
check "중첩된 자리의 반례도 찾는다"
|
||||
(type_has
|
||||
(e3
|
||||
^ "fn f(o: Option[E]) -> Int {\n\
|
||||
\ match o {\n\
|
||||
\ Some(A(n)) => n,\n\
|
||||
\ None => 0,\n\
|
||||
\ }\n\
|
||||
}")
|
||||
"Some(B)");
|
||||
check "Int 리터럴만으로는 완전해지지 않는다"
|
||||
(type_has
|
||||
"fn f(n: Int) -> Int {\n match n {\n 0 => 1,\n 1 => 2,\n }\n}"
|
||||
"모든 경우를 덮지 않습니다");
|
||||
check "와일드카드가 있으면 리터럴 match도 통과"
|
||||
(type_ok
|
||||
"fn f(n: Int) -> Int {\n match n {\n 0 => 1,\n _ => 2,\n }\n}")
|
||||
|
||||
let () =
|
||||
check "와일드카드 뒤의 팔은 도달할 수 없다"
|
||||
(type_has
|
||||
(e3
|
||||
^ "fn f(x: E) -> Int {\n match x {\n _ => 0,\n B => 1,\n }\n}")
|
||||
"도달할 수 없습니다");
|
||||
check "같은 생성자를 두 번 쓰면 뒤가 죽는다"
|
||||
(type_has
|
||||
(e3
|
||||
^ "fn f(x: E) -> Int {\n\
|
||||
\ match x {\n\
|
||||
\ A(n) => n,\n\
|
||||
\ A(m) => m,\n\
|
||||
\ _ => 0,\n\
|
||||
\ }\n\
|
||||
}")
|
||||
"도달할 수 없습니다");
|
||||
check "생성자 집합을 모르면 검사하지 않는다"
|
||||
(type_ok "fn f(w: Widget) -> Int {\n match w {\n _ => 0,\n }\n}")
|
||||
|
||||
(* upstream의 variant 추가가 downstream match를 깨뜨린다 —
|
||||
interface hash가 enum 본문을 입력으로 삼는 이유 *)
|
||||
let () =
|
||||
let two = "enum E {\n A,\n B,\n}\n" in
|
||||
let three = "enum E {\n A,\n B,\n C,\n}\n" in
|
||||
let user =
|
||||
"fn f(x: E) -> Int {\n match x {\n A => 0,\n B => 1,\n }\n}"
|
||||
in
|
||||
check "variant 둘일 때는 통과" (type_ok (two ^ user));
|
||||
check "variant가 늘면 같은 코드가 깨진다" (type_has (three ^ user) "빠진 경우: C")
|
||||
|
||||
Reference in New Issue
Block a user