lint: 파서 오류 복구와 두 lint — 남은 부채를 턴다

파서 오류 복구:
항목 단위로만 회복한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을
하게 되고, 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가
상한이라는 것은 정직한 한계다. 동기화 지점은 중괄호 깊이 0 + 줄 첫머리
+ 선언 시작 토큰 — 셋 다 필요하다. 본문 안의 fn을 새 항목으로 오인하면
그 뒤가 전부 어긋난다. 샘플 08이 이제 오류 넷을 한 번에 보고한다.

두 lint (취향이 아니라 비용이다):
- 미사용 import는 재검사 범위를 넓힌다. 쓰지 않는 모듈의 시그니처가
  바뀌면 이 모듈이 재검사된다.
- effect 과잉 선언은 호출자에게 없는 의무를 지운다. 시그니처는 실제보다
  좁아도 안 되고 넓어도 안 된다.

과잉 선언은 effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지
않는다. 첫 구현이 샘플 03/06을 오탐으로 잡았는데, 원인이 외부 타입이었다
— 외부 capability의 메서드는 effect를 모르므로 "수행하지 않았다"고 말할
근거가 없다. saw_unknown으로 판정을 보류한다.

Resolve.error에 blocking을 나눴다. 이름 해소 실패는 뒤 단계를 막지만
lint는 막지 않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.

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 15:53:35 +09:00
co-authored by Claude Opus 5
parent 5593b54772
commit 91f3840d19
9 changed files with 344 additions and 50 deletions
+107 -29
View File
@@ -453,14 +453,17 @@ let () =
check "effect 집합의 capability도 표면에 든다"
(resolve_ext "fn f() effects {Gw.pay}" = [ "Gw" ])
(* --- 샘플: 01~07은 이름 해소를 통과해야 한다 --- *)
(* --- 샘플: 오류 샘플을 뺀 나머지는 이름 해소를 통과해야 한다 --- *)
(* 일부러 틀린 파일들. 무엇이 틀렸는지는 각 파일의 주석에 있다. *)
let error_samples = [ "08_syntax_errors.cool"; "13_lints.cool" ]
let () =
let dir = "../samples" in
let 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")
|> List.filter (fun f -> not (List.mem f error_samples))
|> List.sort compare
in
List.iter
@@ -580,11 +583,17 @@ let () =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.filter (fun f ->
f <> "05_move_errors.cool"
&& f <> "08_syntax_errors.cool"
&& f <> "09_type_errors.cool"
&& f <> "10_effect_errors.cool"
&& f <> "11_exhaustiveness.cool")
not
(List.mem f
[
"05_move_errors.cool";
"08_syntax_errors.cool";
"09_type_errors.cool";
"10_effect_errors.cool";
"11_exhaustiveness.cool";
"12_stdlib_effects.cool";
"13_lints.cool";
]))
|> List.sort compare
in
List.iter
@@ -1003,17 +1012,23 @@ let run_src src =
let st = Session.create ~root:dir ~std:"../std" () in
Session.run st f
(* prelude가 없다. 표준 라이브러리도 명시적으로 가져온다. *)
(* prelude가 없다. 표준 라이브러리도 명시적으로 가져온다.
미사용 import는 오류이므로 테스트마다 쓰는 것만 가져온다. *)
let imports names =
String.concat ""
(List.map
(fun n ->
Printf.sprintf "import \"cool.dev/std/%s\" as %s\n"
(String.lowercase_ascii n) n)
names)
let console =
"import \"cool.dev/std/list\" as List\n\
import \"cool.dev/std/string\" as String\n\
import \"cool.dev/std/int\" as Int\n\n\
pub capability Console {\n\
"pub capability Console {\n\
\ fn print(s: String) effects {Console.print}\n\
}\n\n"
let outputs src expected =
match run_src (console ^ src) with
let outputs ?(use = []) src expected =
match run_src (imports use ^ console ^ src) with
| Ok out -> out = expected
| Error e ->
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
@@ -1021,13 +1036,13 @@ let outputs src expected =
let () =
check "산술과 출력"
(outputs
(outputs ~use:[ "Int" ]
"pub fn main(c: Console) effects {Console.print} {\n\
\ c.print(Int.show(2 + 3 * 4))\n\
}"
"14\n");
check "match와 생성자"
(outputs
(outputs ~use:[ "Int" ]
"pub enum S {\n\
\ A(Int),\n\
\ B,\n\
@@ -1044,7 +1059,7 @@ let () =
}"
"42\n0\n");
check "mut 바인딩과 대입"
(outputs
(outputs ~use:[ "Int" ]
"pub fn main(c: Console) effects {Console.print} {\n\
\ let mut n = 1\n\
\ n = n + 10\n\
@@ -1052,7 +1067,7 @@ let () =
}"
"11\n");
check "?는 Err에서 즉시 반환한다"
(outputs
(outputs ~use:[ "Int" ]
"pub enum E {\n\
\ Bad,\n\
}\n\n\
@@ -1076,7 +1091,7 @@ let () =
}"
"6\nerr\n");
check "클로저가 바깥 capability를 잡는다"
(outputs
(outputs ~use:[ "List"; "Int" ]
"pub fn main(c: Console) effects {Console.print} {\n\
\ List.each([1, 2], fn(n) {\n\
\ c.print(Int.show(n))\n\
@@ -1102,7 +1117,7 @@ let () =
"pub capability Db {\n\
\ fn read() effects {Db.read} -> Int\n\
}\n\n\
pub fn main(d: Db) effects {Db.read} { }"
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
with
| Error e -> has_sub e.message "제공하지 않습니다"
| Ok _ -> false)
@@ -1121,23 +1136,19 @@ let () =
Session.load st f;
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
in
let hdr =
"import \"cool.dev/std/list\" as List\n\
import \"cool.dev/std/int\" as Int\n\n\
pub capability Console {\n\
\ fn print(s: String) effects {Console.print}\n\
}\n\n"
in
let hdr use = imports use ^ "\n" ^ console in
let list_only = hdr [ "List" ] in
let hdr = hdr [ "List"; "Int" ] in
check "std 시그니처로 인자 개수를 잡는다"
(List.exists
(fun m -> has_sub m "인자 1개가 필요한데")
(std_check
(hdr ^ "pub fn f(xs: List[Int]) -> Int {\n List.len(xs, 1)\n}")));
(list_only ^ "pub fn f(xs: List[Int]) -> Int {\n List.len(xs, 1)\n}")));
check "std 시그니처로 반환 타입을 잡는다"
(List.exists
(fun m -> has_sub m "String이(가) 필요한데 Int")
(std_check
(hdr ^ "pub fn f(xs: List[Int]) -> String {\n List.len(xs)\n}")));
(list_only ^ "pub fn f(xs: List[Int]) -> String {\n List.len(xs)\n}")));
(* effect 변수가 호출 지점에서 실제로 해소된다 *)
check "List.each의 effect 변수가 클로저의 effect로 묶인다"
(List.exists
@@ -1154,3 +1165,70 @@ let () =
\ List.each(xs, fn(n) { c.print(Int.show(n)) })\n\
}")
= [])
(* ------------------------------------------------------------------ *)
(* lint 둘 *)
(* *)
(* 취향이 아니라 비용이다. 미사용 import는 재검사 범위를 넓히고, *)
(* effect 과잉 선언은 호출자에게 없는 의무를 지운다. *)
(* ------------------------------------------------------------------ *)
let () =
let msgs src =
let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_linttest" in
ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
let f = Filename.concat dir "m.cool" in
write f src;
let st = Session.create ~root:dir ~std:"../std" () in
Session.load st f;
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
in
let db =
"pub capability Db {\n\
\ fn read(id: Int) effects {Db.read} -> Int\n\
\ fn write(id: Int) effects {Db.write}\n\
}\n\n"
in
check "미사용 import를 잡는다"
(List.exists
(fun m -> has_sub m "가져왔지만 쓰지 않습니다")
(msgs
"import \"cool.dev/std/list\" as List\n\npub fn f() -> Int {\n 1\n}"));
check "쓰면 잡지 않는다"
(msgs
"import \"cool.dev/std/list\" as List\n\n\
pub fn f(xs: List[Int]) -> Int {\n\
\ List.len(xs)\n\
}"
= []);
check "effect 과잉 선언을 잡는다"
(List.exists
(fun m -> has_sub m "선언했지만 수행하지 않습니다")
(msgs
(db
^ "pub fn f(db: Db, id: Int) effects {Db.read, Db.write} -> Int {\n\
\ db.read(id)\n\
}")));
check "정확히 선언하면 통과한다"
(msgs
(db
^ "pub fn f(db: Db, id: Int) effects {Db.read} -> Int {\n db.read(id)\n}"
)
= []);
(* 모르는 것을 틀렸다고 말하지 않는다 *)
check "effect 변수가 있으면 과잉 선언을 판정하지 않는다"
(msgs "pub fn f[e: effects](g: fn() effects e) effects e {\n g()\n}" = []);
check "외부 타입이 섞이면 과잉 선언을 판정하지 않는다"
(msgs "pub fn f(fs: FileSystem) effects {FileSystem.read} {\n fs.read()\n}"
= []);
(* lint는 뒤 단계를 막지 않는다 — lint 하나가 진짜 타입 오류를 가리면 안 된다 *)
check "미사용 import가 타입 오류를 가리지 않는다"
(let ms =
msgs
"import \"cool.dev/std/list\" as List\n\n\
pub fn f() -> String {\n\
\ 1\n\
}"
in
List.exists (fun m -> has_sub m "가져왔지만 쓰지 않습니다") ms
&& List.exists (fun m -> has_sub m "String이(가) 필요한데 Int") ms)