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:
+65
-6
@@ -784,20 +784,79 @@ let parse_item st =
|
||||
I_const { pub; name; ty; value; pos = p }
|
||||
| _ -> err_expect st "선언 (fn, struct, enum, capability, const)")
|
||||
|
||||
(* ------------------------------------------------------------------ *)
|
||||
(* 오류 복구 *)
|
||||
(* *)
|
||||
(* 항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서 *)
|
||||
(* 다시 시작한다 — 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고, *)
|
||||
(* 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은 *)
|
||||
(* 정직한 한계이지 숨길 것이 아니다. *)
|
||||
(* *)
|
||||
(* 동기화 지점: 중괄호 깊이 0이고, 줄 첫머리이며, 선언을 시작할 수 있는 토큰. *)
|
||||
(* 세 조건이 다 필요하다. 본문 안의 fn을 새 항목으로 오인하면 그 뒤가 전부 *)
|
||||
(* 어긋난다. *)
|
||||
|
||||
let item_starts =
|
||||
[
|
||||
Token.Kw_import;
|
||||
Token.Kw_reexport;
|
||||
Token.Kw_pub;
|
||||
Token.Kw_fn;
|
||||
Token.Kw_struct;
|
||||
Token.Kw_enum;
|
||||
Token.Kw_capability;
|
||||
Token.Kw_const;
|
||||
Token.Kw_copyable;
|
||||
]
|
||||
|
||||
let at_line_start st = st.i > 0 && st.toks.(st.i - 1).Token.kind = Token.Newline
|
||||
|
||||
let sync st =
|
||||
let depth = ref 0 in
|
||||
let fin = ref false in
|
||||
while not !fin do
|
||||
match kind st with
|
||||
| Token.Eof -> fin := true
|
||||
| Token.LBrace ->
|
||||
incr depth;
|
||||
adv st
|
||||
| Token.RBrace ->
|
||||
decr depth;
|
||||
adv st
|
||||
| k ->
|
||||
if !depth <= 0 && at_line_start st && List.mem k item_starts then
|
||||
fin := true
|
||||
else adv st
|
||||
done
|
||||
|
||||
let parse_module st =
|
||||
skip_nl st;
|
||||
let errors = ref [] in
|
||||
let rec loop acc =
|
||||
if kind st = Token.Eof then List.rev acc
|
||||
else
|
||||
let it = parse_item st in
|
||||
skip_nl st;
|
||||
loop (it :: acc)
|
||||
match parse_item st with
|
||||
| it ->
|
||||
skip_nl st;
|
||||
loop (it :: acc)
|
||||
| exception Error e ->
|
||||
errors := e :: !errors;
|
||||
let before = st.i in
|
||||
sync st;
|
||||
(* 진행 보장. 같은 자리에서 다시 실패하면 무한 루프다. *)
|
||||
if st.i = before then adv st;
|
||||
skip_nl st;
|
||||
loop acc
|
||||
in
|
||||
{ items = loop [] }
|
||||
let items = loop [] in
|
||||
({ items }, List.rev !errors)
|
||||
|
||||
let parse (tokens : Token.t list) : modul =
|
||||
let parse_all (tokens : Token.t list) : modul * error list =
|
||||
let st = { toks = Array.of_list tokens; i = 0; no_struct = false } in
|
||||
parse_module st
|
||||
|
||||
let parse (tokens : Token.t list) : modul =
|
||||
match parse_all tokens with m, [] -> m | _, e :: _ -> raise (Error e)
|
||||
|
||||
let parse_result tokens =
|
||||
match parse tokens with m -> Ok m | exception Error e -> Error e
|
||||
match parse_all tokens with m, [] -> Ok m | _, e :: _ -> Error e
|
||||
|
||||
+32
-3
@@ -11,7 +11,10 @@
|
||||
|
||||
open Ast
|
||||
|
||||
type error = { pos : Token.pos; msg : string }
|
||||
(* blocking: 이름을 해소하지 못했다는 뜻이고, 이후 단계는 의미가 없다.
|
||||
lint는 다르다 — 코드는 분석 가능하고 검사도 계속되어야 한다.
|
||||
둘을 같은 통에 넣으면 lint 하나가 진짜 타입 오류를 가린다. *)
|
||||
type error = { pos : Token.pos; msg : string; blocking : bool }
|
||||
type info = { externals : (string * Token.pos) list }
|
||||
type item_kind = K_fn | K_type | K_const | K_import
|
||||
|
||||
@@ -23,13 +26,20 @@ type state = {
|
||||
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
|
||||
mutable errors : error list;
|
||||
mutable ext : (string * Token.pos) list;
|
||||
(* 실제로 참조된 import 별칭 *)
|
||||
used : (string, unit) Hashtbl.t;
|
||||
}
|
||||
|
||||
let builtin_types =
|
||||
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
|
||||
|
||||
let builtin_values = [ "unit"; "Ok"; "Err"; "Some"; "None" ]
|
||||
let error st pos msg = st.errors <- { pos; msg } :: st.errors
|
||||
let error st pos msg = st.errors <- { pos; msg; blocking = true } :: st.errors
|
||||
let lint st pos msg = st.errors <- { pos; msg; blocking = false } :: st.errors
|
||||
|
||||
let use_alias st a =
|
||||
if Hashtbl.find_opt st.items a = Some K_import then
|
||||
Hashtbl.replace st.used a ()
|
||||
|
||||
let external_ref st name pos =
|
||||
if not (List.mem_assoc name st.ext) then st.ext <- (name, pos) :: st.ext
|
||||
@@ -80,6 +90,7 @@ let rec resolve_ty st = function
|
||||
(* 한정된 이름은 별칭이 이 모듈에 있는지만 본다. 그 모듈 안에 그 타입이
|
||||
있는지는 모듈 하나만 보고 결정할 수 없다 — 외부 참조로 기록한다. *)
|
||||
| T_named { modl = Some a; args; pos; _ } ->
|
||||
use_alias st a;
|
||||
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||
List.iter (resolve_targ st pos) args
|
||||
| T_named { modl = None; name; args; pos } ->
|
||||
@@ -124,6 +135,7 @@ let rec resolve_pattern st seen = function
|
||||
seen := n :: !seen;
|
||||
bind st pos n false))
|
||||
| P_ctor { modl = Some a; args; pos; _ } ->
|
||||
use_alias st a;
|
||||
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||
List.iter (resolve_pattern st seen) args
|
||||
| P_ctor { modl = None; name; args; pos } ->
|
||||
@@ -199,7 +211,12 @@ let rec resolve_expr st = function
|
||||
| E_call { callee; args; _ } ->
|
||||
resolve_expr st callee;
|
||||
List.iter (resolve_expr st) args
|
||||
| E_field { obj; _ } -> resolve_expr st obj
|
||||
| E_field { obj; _ } ->
|
||||
(* Alias.f — 별칭 접근도 사용이다 *)
|
||||
(match obj with
|
||||
| E_ident (o, _) when lookup_local st o = None -> use_alias st o
|
||||
| _ -> ());
|
||||
resolve_expr st obj
|
||||
| E_inst { callee; args; pos } ->
|
||||
resolve_expr st callee;
|
||||
List.iter (resolve_targ st pos) args
|
||||
@@ -312,6 +329,7 @@ let resolve (m : modul) : info * error list =
|
||||
scopes = [];
|
||||
errors = [];
|
||||
ext = [];
|
||||
used = Hashtbl.create 8;
|
||||
}
|
||||
in
|
||||
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
|
||||
@@ -362,6 +380,17 @@ let resolve (m : modul) : info * error list =
|
||||
resolve_expr st value;
|
||||
pop st)
|
||||
m.items;
|
||||
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
|
||||
쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다. 증분 루프의
|
||||
비용을 아무 이유 없이 넓히는 선언은 남겨둘 수 없다. *)
|
||||
List.iter
|
||||
(fun it ->
|
||||
match it with
|
||||
| I_import { alias; pos; _ } when not (Hashtbl.mem st.used alias) ->
|
||||
lint st pos
|
||||
(Printf.sprintf "%s을(를) 가져왔지만 쓰지 않습니다 (재검사 범위만 넓힙니다)" alias)
|
||||
| _ -> ())
|
||||
m.items;
|
||||
let by_pos (_, a) (_, b) =
|
||||
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
|
||||
in
|
||||
|
||||
+19
-10
@@ -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)
|
||||
|
||||
+39
-1
@@ -35,6 +35,9 @@ type env = {
|
||||
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
|
||||
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
|
||||
mutable performed : (T.atom * Token.pos) list;
|
||||
(* 이 본문에서 모르는 것을 만났는가. 과잉 선언 판정에만 쓴다 — 외부 타입의
|
||||
메서드는 effect를 알 수 없으므로 "수행하지 않았다"고 말할 근거가 없다. *)
|
||||
mutable saw_unknown : bool;
|
||||
mutable errors : error list;
|
||||
}
|
||||
|
||||
@@ -182,7 +185,9 @@ let rec infer env (e : expr) : T.t =
|
||||
| None -> (
|
||||
match Hashtbl.find_opt env.ctors n with
|
||||
| Some enum -> nullary_ctor env enum n
|
||||
| None -> T.TUnknown)))))
|
||||
| None ->
|
||||
env.saw_unknown <- true;
|
||||
T.TUnknown)))))
|
||||
| E_list (xs, pos) ->
|
||||
let elem = T.fresh () in
|
||||
List.iter
|
||||
@@ -429,6 +434,7 @@ and infer_call env callee args pos =
|
||||
in
|
||||
match fn_ty with
|
||||
| None ->
|
||||
env.saw_unknown <- true;
|
||||
List.iter (fun a -> ignore (infer env a)) args;
|
||||
T.TUnknown
|
||||
| Some (T.TFn { params; eff; ret; _ }) ->
|
||||
@@ -678,6 +684,7 @@ let check_fn env (d : fn_decl) =
|
||||
in
|
||||
env.ret <- declared;
|
||||
env.performed <- [];
|
||||
env.saw_unknown <- false;
|
||||
let got = infer_block env body in
|
||||
if not (T.unify declared got) then
|
||||
mismatch env d.fn_pos declared got
|
||||
@@ -699,6 +706,36 @@ let check_fn env (d : fn_decl) =
|
||||
(T.atom_show m) d.fn_name (T.eff_show declared_eff)))
|
||||
missing)
|
||||
(List.rev env.performed);
|
||||
(* 과잉 선언도 오류다. 선언한 effect를 수행하지 않으면 호출자는 하지도
|
||||
않는 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를
|
||||
받아오게 된다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||
|
||||
effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출
|
||||
지점이 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고
|
||||
말하지 않는다. *)
|
||||
let has_var =
|
||||
List.exists (function T.A_var _ -> true | _ -> false) declared_eff
|
||||
in
|
||||
if (not has_var) && not env.saw_unknown then
|
||||
List.iter
|
||||
(fun a ->
|
||||
match a with
|
||||
| T.A_name (cap, meth) ->
|
||||
let performed = List.map fst env.performed |> T.eff_resolve in
|
||||
if
|
||||
not
|
||||
(List.exists
|
||||
(function
|
||||
| T.A_name (c, m) -> c = cap && m = meth
|
||||
| T.A_var _ | T.A_meta _ -> true)
|
||||
performed)
|
||||
then
|
||||
err env d.fn_pos
|
||||
(Printf.sprintf
|
||||
"%s은(는) %s을(를) 선언했지만 수행하지 않습니다 (effects 절에서 지우십시오)"
|
||||
d.fn_name (T.atom_show a))
|
||||
| _ -> ())
|
||||
declared_eff;
|
||||
env.performed <- [];
|
||||
pop env
|
||||
|
||||
@@ -735,6 +772,7 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
||||
locals = [];
|
||||
ret = T.TUnit;
|
||||
performed = [];
|
||||
saw_unknown = false;
|
||||
errors = [];
|
||||
}
|
||||
in
|
||||
|
||||
Reference in New Issue
Block a user