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:
+107
-29
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user