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
+136 -7
View File
@@ -306,9 +306,10 @@ let () =
check "match"
(one "fn f() {\n match e {\n A(_) => 1,\n _ => 2,\n }\n}"
= "(fn f () (block (match e ((A _) => 1) (_ => 2))))");
check "scope"
(one "fn f() {\n scope sc {\n sc.spawn(g)\n }\n}"
= "(fn f () (block (scope sc (block (call (. sc spawn) g)))))");
check "scope는 부모를 명시한다"
(one "fn f(root: TaskScope) {\n scope sc = root {\n sc.spawn(g)\n }\n}"
= "(fn f (root:TaskScope) (block (scope sc = root (block (call (. sc \
spawn) g)))))");
check "struct 리터럴"
(one "fn f() {\n H { a: 1 }\n}" = "(fn f () (block (struct H (a 1))))")
@@ -411,10 +412,15 @@ let () =
(* --- scope 머리는 지역 바인딩이어야 한다 --- *)
let () =
check "지역 바인딩이 아닌 scope 이름"
(has_err "fn f() {\n scope sc {\n sc\n }\n}" "지역 바인딩이 아닙니다");
check "파라미터로 온 scope는 통과"
(resolve_errs "fn f(sc: TaskScope) {\n scope sc {\n sc\n }\n}" = [])
check "부모 scope가 지역 바인딩이 아니면 오류"
(has_err "fn f() {\n scope s = root {\n s\n }\n}" "지역 바인딩이 아닙니다");
check "파라미터로 온 부모는 통과"
(resolve_errs "fn f(root: TaskScope) {\n scope s = root {\n s\n }\n}"
= []);
check "자식 scope 이름은 블록 안에서만 산다"
(resolve_ext
"fn f(root: TaskScope) {\n scope s = root {\n s\n }\n s\n}"
= [ "TaskScope"; "s" ])
(* --- variant는 구문이 아니라 이름 해소가 판정한다 --- *)
@@ -468,3 +474,126 @@ let () =
errors;
check (f ^ " 이름 해소") false)
files
(* ================================================================== *)
(* 타입 검사 *)
(* ================================================================== *)
let type_errs src =
List.map (fun (e : Typecheck.error) -> e.msg) (Typecheck.check (parse_ok src))
let type_ok src = type_errs src = []
let type_has src frag =
List.exists
(fun m ->
let n = String.length frag in
let rec go i =
i + n <= String.length m && (String.sub m i n = frag || go (i + 1))
in
go 0)
(type_errs src)
(* --- 제네릭은 호출 지점에서 지역 unification으로 풀린다 --- *)
let () =
check "제네릭 인스턴스화" (type_ok "fn id[a](x: a) -> a\nfn f() -> Int {\n id(1)\n}");
check "제네릭 결과가 반환 타입과 안 맞으면 오류"
(type_has "fn id[a](x: a) -> a\nfn f() -> String {\n id(1)\n}" "String");
check "명시적 인스턴스화의 인자 개수"
(type_has "fn id[a](x: a) -> a\nfn f() -> Int {\n id[Int, Int](1)\n}"
"타입 인자 1개가 필요한데 2개");
check "고차 함수의 클로저 파라미터 타입은 기대 타입에서 온다"
(type_ok
"fn map[a, b](xs: List[a], f: fn(a) -> b) -> List[b]\n\
fn g(xs: List[Int]) -> List[Int] {\n\
\ map(xs, fn(x) { x + 1 })\n\
}");
check "클로저 본문의 타입 오류는 잡힌다"
(type_has
"fn map[a, b](xs: List[a], f: fn(a) -> b) -> List[b]\n\
fn g(xs: List[Int]) -> List[Int] {\n\
\ map(xs, fn(x) { x + \"1\" })\n\
}"
"산술 연산자")
(* --- Result와 ? --- *)
let () =
check "?는 Result를 벗긴다"
(type_ok
"fn f(x: Result[Int, String]) -> Result[Int, String] {\n\
\ let a = x?\n\
\ Ok(a)\n\
}");
check "?는 Result 반환 함수 안에서만"
(type_has "fn f(x: Result[Int, String]) -> Int {\n x?\n}"
"Result를 반환하는 함수 안에서만")
(* --- enum과 struct --- *)
let () =
let e = "enum E {\n A(Int),\n B,\n}\n" in
check "생성자 호출" (type_ok (e ^ "fn f() -> E {\n A(1)\n}"));
check "생성자 인자 타입" (type_has (e ^ "fn f() -> E {\n A(\"x\")\n}") "인자");
check "인자 없는 생성자" (type_ok (e ^ "fn f() -> E {\n B\n}"));
check "match는 값을 낸다"
(type_ok
(e
^ "fn f(x: E) -> Int {\n match x {\n A(n) => n,\n B => 0,\n }\n}"
));
check "제네릭 struct 필드"
(type_ok "struct P[a] {\n v: a,\n}\nfn f(p: P[Int]) -> Int {\n p.v\n}");
check "제네릭 struct 필드 타입 오류"
(type_has "struct P[a] {\n v: a,\n}\nfn f(p: P[Int]) -> String {\n p.v\n}"
"String")
(* --- capability 메서드 --- *)
let () =
let c = "capability G {\n fn pay(n: Int) -> Bool\n}\n" in
check "capability 메서드 타입"
(type_ok (c ^ "fn f(g: G) -> Bool {\n g.pay(1)\n}"));
check "없는 메서드"
(type_has (c ^ "fn f(g: G) -> Bool {\n g.nope(1)\n}") "메서드가 없습니다");
check "메서드 인자 타입"
(type_has (c ^ "fn f(g: G) -> Bool {\n g.pay(\"x\")\n}") "인자")
(* --- 외부 이름은 검사를 막지 않는다 --- *)
let () =
check "모르는 타입은 무엇과도 맞는다"
(type_ok "fn f(w: Widget) -> Int {\n w.anything(1, 2)\n}");
check "모르는 것을 틀렸다고 말하지 않는다" (type_ok "fn f(w: Widget) -> Widget {\n w\n}")
(* --- affinity는 타입 검사가 소유하지 않는다 --- *)
let () =
check "affine fn과 fn은 타입 동등성에서 구분되지 않는다"
(type_ok "fn f() -> affine fn() {\n fn() {\n unit\n }\n}")
(* --- 샘플 --- *)
let () =
let dir = "../samples" in
let ok_files =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.filter (fun f ->
f <> "08_syntax_errors.cool" && f <> "09_type_errors.cool")
|> List.sort compare
in
List.iter
(fun f ->
match Driver.typecheck (Filename.concat dir f) with
| Ok () -> ()
| Error errors ->
List.iter
(fun e -> Printf.printf " %s\n" (Driver.string_of_error e))
errors;
check (f ^ " 타입 검사") false)
ok_files;
match Driver.typecheck (Filename.concat dir "09_type_errors.cool") with
| Ok () -> check "09는 타입 오류를 내야 한다" false
| Error errors ->
check "09의 오류를 전부 모은다 (첫 오류에서 멈추지 않는다)" (List.length errors >= 18)