typecheck: 타입 검사기와 scope 부모 문법 확정

세 가지를 자율 결정으로 닫고 타입 검사까지 세웠다.

1. scope 부모 문법: scope 자식 = 부모 { ... }로 확정. 부모를 적지 않으면
   자식의 부모가 "가장 가까운 스코프"가 되는데 그것이 정확히 ambient
   authority다. 권한 사슬이 main의 루트 TaskScope부터 끊기지 않으려면 모든
   자식이 부모를 이름으로 지목해야 한다. 이름 해소가 이 구멍을 잡아준 건이라,
   주석 처리했던 중첩 예제를 되살렸다.

2. prelude: 타입 이름은 그 타입에 딸린 함수의 이름공간이다(String.len,
   File.close). 정적 메서드를 위한 별도 문법을 두지 않는다.

3. 타입 검사: 이 모듈 안에서 아는 것만 검사한다. 외부 이름은 TUnknown이
   되어 무엇과도 맞는다 — 모르는 것을 틀렸다고 말하지 않기 위해서다.
   제네릭 해소는 호출 지점의 지역 unification이고 함수 하나를 넘지 않는다.
   클로저 파라미터 타입은 기대 타입에서 읽어온다(양방향 검사, 로컬).

단계 소유권을 하나 정정했다. affinity는 타입 동등성의 일부가 아니다. 값이
affine인지는 무엇을 capture했는지로 정해지는 substructural 성질이고
move/affinity 검사가 소유한다. 타입 검사가 이걸 판정하려다 정당한 코드를
거부하는 것을 06에서 확인하고 unify에서 분리했다.

unify 버그 하나: 같은 미지수끼리 unify할 때 occurs check가 자기 자신을
발견해 실패하고 있었다. 02의 fold 호출에서 잡혔다.

09_type_errors.cool 추가 — 외부 타입이 하나도 없어 검사기가 TUnknown으로
빠져나갈 구석이 없는 파일이다. 18개 진단이 전부 잡히고, 첫 오류에서 멈추지
않고 모두 보고한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 02:42:35 +09:00
co-authored by Claude Opus 5
parent 94e28d427b
commit 5e034c0891
13 changed files with 1072 additions and 70 deletions
+93
View File
@@ -0,0 +1,93 @@
(* 타입 표현과 지역 unification.
TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서
해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을
틀렸다고 말하지 않기 위해서다. 아는 범위에서만 검사한다.
TMeta는 호출 지점에서 제네릭을 인스턴스화할 때 생기는 미지수다. 함수 하나
범위에서만 살고 전역으로 흐르지 않는다 (철학 2: 전역 추론 없음). *)
type t =
| TUnknown
| TInt
| TBool
| TString
| TUnit
| TVar of string
| TCon of string * t list
| TFn of { affine : bool; params : t list; ret : t }
| TMeta of meta ref
and meta = Unbound of int | Bound of t
let counter = ref 0
let fresh () =
incr counter;
TMeta (ref (Unbound !counter))
let rec resolve t =
match t with TMeta { contents = Bound u } -> resolve u | _ -> t
let rec show t =
match resolve t with
| TUnknown -> "?"
| TInt -> "Int"
| TBool -> "Bool"
| TString -> "String"
| TUnit -> "Unit"
| TVar v -> v
| TCon (n, []) -> n
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
| TFn { affine; params; ret } -> (
(if affine then "affine fn(" else "fn(")
^ String.concat ", " (List.map show params)
^ ")"
^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r)
| TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n
| TMeta { contents = Bound _ } -> "?"
let rec occurs r t =
match resolve t with
| TMeta r' -> r == r'
| TCon (_, args) -> List.exists (occurs r) args
| TFn { params; ret; _ } -> List.exists (occurs r) params || occurs r ret
| _ -> false
(* 성공하면 true. 실패해도 예외를 던지지 않는다 — 호출자가 위치를 알고
진단을 만든다. *)
let rec unify a b =
match (resolve a, resolve b) with
| TUnknown, _ | _, TUnknown -> true
| TMeta r, TMeta r' when r == r' -> true
| TMeta r, t | t, TMeta r ->
if occurs r t then false
else (
r := Bound t;
true)
| TInt, TInt | TBool, TBool | TString, TString | TUnit, TUnit -> true
| TVar x, TVar y -> x = y
| TCon (n, xs), TCon (m, ys) ->
n = m && List.length xs = List.length ys && List.for_all2 unify xs ys
| TFn f, TFn g ->
(* affinity는 타입 동등성의 일부가 아니다. 값이 affine인지는 무엇을
capture했는지로 정해지는 substructural 성질이고, move/affinity 검사가
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
List.length f.params = List.length g.params
&& List.for_all2 unify f.params g.params
&& unify f.ret g.ret
| _ -> false
(* 제네릭 인스턴스화: TVar를 주어진 대입으로 바꾼다 *)
let rec subst env t =
match resolve t with
| TVar v -> ( match List.assoc_opt v env with Some u -> u | None -> TVar v)
| TCon (n, args) -> TCon (n, List.map (subst env) args)
| TFn f ->
TFn
{
affine = f.affine;
params = List.map (subst env) f.params;
ret = subst env f.ret;
}
| u -> u