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
+19 -10
View File
@@ -98,13 +98,17 @@ let read_file file =
let err_of file (pos : Token.pos) msg =
{ file; line = pos.line; col = pos.col; message = msg }
(* 파서는 항목 단위로 회복하므로 오류가 여럿일 수 있다. 첫 오류에서 멈추면
고칠 때마다 다시 돌려야 하고, 그것이 빠른 루프의 반대다. *)
let parse_file file =
match Lexer.lex_result (read_file file) with
| Error e -> Error (err_of file e.pos e.msg)
| Error e -> Error [ err_of file e.pos e.msg ]
| Ok toks -> (
match Parser.parse_result toks with
| Error e -> Error (err_of file e.pos e.msg)
| Ok m -> Ok m)
match Parser.parse_all toks with
| m, [] -> Ok m
| _, errs ->
Error
(List.map (fun (e : Parser.error) -> err_of file e.pos e.msg) errs))
let imports_of (m : Ast.modul) =
List.filter_map
@@ -116,13 +120,13 @@ let imports_of (m : Ast.modul) =
let check_module st path : entry =
st.checked <- path :: st.checked;
match parse_file path with
| Error e ->
| Error es ->
{
path;
ast = { items = [] };
imports = [];
iface = { items = []; hash = "" };
errors = [ e ];
errors = es;
}
| Ok ast ->
let imports =
@@ -145,15 +149,20 @@ let check_module st path : entry =
in
let iface = Iface.of_module ast in
let _, rerrors = Resolve.resolve ast in
(* 이름을 해소하지 못했으면 뒤 단계는 의미가 없다. lint는 막지 않는다. *)
let blocking =
List.filter (fun (e : Resolve.error) -> e.blocking) rerrors
in
let errors =
List.map (fun (e : Resolve.error) -> err_of path e.pos e.msg) rerrors
in
let errors =
if errors <> [] then errors
if blocking <> [] then errors
else
List.map
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
(Typecheck.check ~imports:dep_surface ast)
errors
@ List.map
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
(Typecheck.check ~imports:dep_surface ast)
@ List.map
(fun (e : Move.error) -> err_of path e.pos e.msg)
(Move.check ~imports:dep_surface ast)