Files
coollang/lib/exhaust.ml
T
coolguyandClaude Opus 5 801a7b330b modules: 모듈 경계, interface hash, 그리고 고정점 invalidation
되돌리기 비싼 결정 중 마지막 하나 — incremental 아키텍처 — 를 코드와
테스트로 닫는다.

- iface.ml: exported surface 추출과 해시. 별칭 한정(qualify)은 소비 시점에만
  일어나므로 가져오는 쪽의 별칭이 정의 모듈의 hash에 새지 않는다.
- session.ml: 모듈 로딩과 고정점 전파. hash 비교가 dependents 재검사보다
  앞선다 — 이 순서가 "본문만 수정 시 downstream 0건"의 전부다.
- 한정 이름(Alias.Type, Alias.Ctor, Alias.fn)을 타입 검사, 패턴, 소진성,
  move 검사가 모두 하나의 키("Alias.name")로 본다.
- 패키지 경로(cool.dev/std/list)는 v0에서 해소하지 않고 불투명하게 둔다.
  없다고 말하지 않는다.
- 회귀 테스트: 본문만 고치면 자기 자신만 재검사(1건), variant를 추가하면
  downstream까지 전파되고 실제로 소진성이 깨진다(2건).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 14:11:30 +09:00

204 lines
7.7 KiB
OCaml

(* match exhaustiveness와 도달 불가 팔 검사 (Maranget의 usefulness 알고리즘).
철학 1의 대표 항목이다. 그리고 interface hash가 enum 정의 본문을 입력으로
삼는 이유이기도 하다 — upstream에 variant가 하나 늘면 downstream의 match가
깨져야 하는데, 이 검사가 없으면 깨질 것이 없다.
생성자 집합을 알 수 없는 타입(외부 타입, 미지수)은 검사하지 않는다.
모르는 것을 위반이라고 말하지 않는다. *)
module T = Types
(* 패턴을 검사용 형태로 줄인다. 바인딩은 와일드카드와 같다 —
무엇을 덮는가만 중요하다. *)
type cpat = CWild | CCtor of string * cpat list
type ctor = { c_name : string; c_args : T.t list }
type ctors = Finite of ctor list | Infinite
type env = {
(* enum 이름 -> variant 목록. 제네릭은 인스턴스화해서 넘어온다 *)
variants : string -> T.t list -> (string * T.t list) list option;
is_ctor : string -> bool;
}
let ctors_of env (t : T.t) : ctors =
match T.resolve t with
| T.TBool ->
Finite
[ { c_name = "true"; c_args = [] }; { c_name = "false"; c_args = [] } ]
| T.TUnit -> Finite [ { c_name = "unit"; c_args = [] } ]
| T.TCon ("Option", [ a ]) ->
Finite
[
{ c_name = "Some"; c_args = [ a ] }; { c_name = "None"; c_args = [] };
]
| T.TCon ("Result", [ a; b ]) ->
Finite
[
{ c_name = "Ok"; c_args = [ a ] }; { c_name = "Err"; c_args = [ b ] };
]
| T.TCon (n, args) -> (
match env.variants n args with
| Some vs ->
Finite
(List.map (fun (name, tys) -> { c_name = name; c_args = tys }) vs)
| None -> Infinite)
| _ -> Infinite
let rec of_pattern env (p : Ast.pattern) : cpat =
match p with
| Ast.P_wild _ -> CWild
| Ast.P_lit (Ast.L_bool true, _) -> CCtor ("true", [])
| Ast.P_lit (Ast.L_bool false, _) -> CCtor ("false", [])
(* 리터럴은 인자 없는 생성자다. 와일드카드로 바꾸면 모든 값을 덮는 것이 되어
Int 리터럴 몇 개로 exhaustive가 되어버린다. 타입의 생성자 집합이 무한하므로
리터럴만으로는 결코 완전해지지 않는다. *)
| Ast.P_lit (Ast.L_int n, _) -> CCtor ("<" ^ n ^ ">", [])
| Ast.P_lit (Ast.L_str v, _) -> CCtor ("<" ^ String.escaped v ^ ">", [])
| Ast.P_bind (n, _) -> if env.is_ctor n then CCtor (n, []) else CWild
| Ast.P_ctor { modl; name; args; _ } ->
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
if env.is_ctor name then CCtor (name, List.map (of_pattern env) args)
else CWild
let wilds n = List.init n (fun _ -> CWild)
(* 행렬을 생성자 c로 특수화한다 *)
let specialize (c : ctor) (matrix : cpat list list) : cpat list list =
let arity = List.length c.c_args in
List.filter_map
(fun row ->
match row with
| CCtor (n, args) :: rest ->
if n = c.c_name then Some (args @ rest) else None
| CWild :: rest -> Some (wilds arity @ rest)
| [] -> None)
matrix
let default_matrix (matrix : cpat list list) : cpat list list =
List.filter_map
(fun row ->
match row with
| CCtor _ :: _ -> None
| CWild :: rest -> Some rest
| [] -> None)
matrix
let head_names (matrix : cpat list list) =
List.filter_map
(fun row -> match row with CCtor (n, _) :: _ -> Some n | _ -> None)
matrix
(* 행렬이 덮지 못하는 반례 벡터를 찾는다. None이면 완전하다. *)
let rec witness env (matrix : cpat list list) (tys : T.t list) :
cpat list option =
match tys with
| [] -> if matrix = [] then Some [] else None
| th :: rest -> (
let heads = head_names matrix in
match ctors_of env th with
| Finite cs
when List.for_all (fun c -> List.mem c.c_name heads) cs && cs <> [] ->
(* 모든 생성자가 나타났다: 각각으로 파고든다 *)
let rec try_each = function
| [] -> None
| c :: more -> (
let arity = List.length c.c_args in
match witness env (specialize c matrix) (c.c_args @ rest) with
| Some ws ->
let args = List.filteri (fun i _ -> i < arity) ws in
let tail = List.filteri (fun i _ -> i >= arity) ws in
Some (CCtor (c.c_name, args) :: tail)
| None -> try_each more)
in
try_each cs
| kind -> (
(* 빠진 생성자가 있거나 집합이 무한하다 *)
match witness env (default_matrix matrix) rest with
| None -> None
| Some ws ->
let head =
match kind with
| Finite cs -> (
match
List.find_opt (fun c -> not (List.mem c.c_name heads)) cs
with
| Some c -> CCtor (c.c_name, wilds (List.length c.c_args))
| None -> CWild)
| Infinite -> CWild
in
Some (head :: ws)))
let rec show_cpat = function
| CWild -> "_"
| CCtor (n, []) -> n
| CCtor (n, args) ->
n ^ "(" ^ String.concat ", " (List.map show_cpat args) ^ ")"
(* 행 q가 행렬 P에 대해 쓸모 있는가 = P가 덮지 못하는 값을 q가 덮는가 *)
let useful env (matrix : cpat list list) (q : cpat list) (tys : T.t list) : bool
=
let rec go matrix q tys =
match (q, tys) with
| [], [] -> matrix = []
| qh :: qt, th :: tt -> (
match qh with
| CCtor (n, args) -> (
match ctors_of env th with
| Finite cs -> (
match List.find_opt (fun c -> c.c_name = n) cs with
| Some c -> go (specialize c matrix) (args @ qt) (c.c_args @ tt)
| None -> go (default_matrix matrix) qt tt)
| Infinite ->
let c =
{ c_name = n; c_args = List.map (fun _ -> T.TUnknown) args }
in
go (specialize c matrix) (args @ qt) (c.c_args @ tt))
| CWild -> (
let heads = head_names matrix in
match ctors_of env th with
| Finite cs
when List.for_all (fun c -> List.mem c.c_name heads) cs
&& cs <> [] ->
List.exists
(fun c ->
go (specialize c matrix)
(wilds (List.length c.c_args) @ qt)
(c.c_args @ tt))
cs
| _ -> go (default_matrix matrix) qt tt))
| _ -> false
in
go matrix q tys
type result = {
missing : string option; (* 빠진 경우의 반례 *)
unreachable : int list; (* 도달할 수 없는 팔의 번호 (0부터) *)
}
let check env (scrutinee : T.t) (pats : Ast.pattern list) : result =
match ctors_of env scrutinee with
| Infinite when T.resolve scrutinee = T.TUnknown ->
(* 생성자 집합을 모르면 검사하지 않는다 *)
{ missing = None; unreachable = [] }
| _ ->
let rows = List.map (fun p -> [ of_pattern env p ]) pats in
let unreachable =
let acc = ref [] in
List.iteri
(fun i row ->
let before = List.filteri (fun j _ -> j < i) rows in
if not (useful env before row [ scrutinee ]) then acc := i :: !acc)
rows;
List.rev !acc
in
let missing =
(* 리터럴 패턴이 섞이면 정확한 반례를 만들 수 없다 — 그 열은 무한
집합이므로 와일드카드가 없으면 불완전으로 본다 *)
match witness env rows [ scrutinee ] with
| Some ws -> (
match ws with [ w ] -> Some (show_cpat w) | _ -> Some "_")
| None -> None
in
{ missing; unreachable }