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:
@@ -338,6 +338,23 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
|
|||||||
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
|
||||||
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
|
||||||
|
|
||||||
|
■ lint (오류다, 경고가 아니다)
|
||||||
|
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||||
|
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
|
||||||
|
- effect 과잉 선언: 선언하고 수행하지 않으면 호출자가 하지도 않는 일에
|
||||||
|
의무를 진다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||||
|
단, effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지 않는다 —
|
||||||
|
무엇이 묶일지는 호출 지점이 정하고, 외부 타입의 메서드는 effect를 모른다.
|
||||||
|
lint는 blocking이 아니다. 이름 해소 실패는 뒤 단계를 막지만 lint는 막지
|
||||||
|
않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.
|
||||||
|
|
||||||
|
■ 파서 오류 복구
|
||||||
|
항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
|
||||||
|
다시 시작한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고,
|
||||||
|
틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은
|
||||||
|
정직한 한계이지 숨길 것이 아니다.
|
||||||
|
동기화 지점: 중괄호 깊이 0 + 줄 첫머리 + 선언 시작 토큰. 셋 다 필요하다.
|
||||||
|
|
||||||
■ 표준 라이브러리 (std/)
|
■ 표준 라이브러리 (std/)
|
||||||
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
|
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
|
||||||
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
|
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
|
||||||
|
|||||||
+65
-6
@@ -784,20 +784,79 @@ let parse_item st =
|
|||||||
I_const { pub; name; ty; value; pos = p }
|
I_const { pub; name; ty; value; pos = p }
|
||||||
| _ -> err_expect st "선언 (fn, struct, enum, capability, const)")
|
| _ -> 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 =
|
let parse_module st =
|
||||||
skip_nl st;
|
skip_nl st;
|
||||||
|
let errors = ref [] in
|
||||||
let rec loop acc =
|
let rec loop acc =
|
||||||
if kind st = Token.Eof then List.rev acc
|
if kind st = Token.Eof then List.rev acc
|
||||||
else
|
else
|
||||||
let it = parse_item st in
|
match parse_item st with
|
||||||
skip_nl st;
|
| it ->
|
||||||
loop (it :: acc)
|
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
|
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
|
let st = { toks = Array.of_list tokens; i = 0; no_struct = false } in
|
||||||
parse_module st
|
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 =
|
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
|
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 info = { externals : (string * Token.pos) list }
|
||||||
type item_kind = K_fn | K_type | K_const | K_import
|
type item_kind = K_fn | K_type | K_const | K_import
|
||||||
|
|
||||||
@@ -23,13 +26,20 @@ type state = {
|
|||||||
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
|
mutable scopes : (string * bool) list list; (* 이름 -> 가변 여부 *)
|
||||||
mutable errors : error list;
|
mutable errors : error list;
|
||||||
mutable ext : (string * Token.pos) list;
|
mutable ext : (string * Token.pos) list;
|
||||||
|
(* 실제로 참조된 import 별칭 *)
|
||||||
|
used : (string, unit) Hashtbl.t;
|
||||||
}
|
}
|
||||||
|
|
||||||
let builtin_types =
|
let builtin_types =
|
||||||
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
|
[ "Int"; "Bool"; "String"; "Unit"; "List"; "Option"; "Result" ]
|
||||||
|
|
||||||
let builtin_values = [ "unit"; "Ok"; "Err"; "Some"; "None" ]
|
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 =
|
let external_ref st name pos =
|
||||||
if not (List.mem_assoc name st.ext) then st.ext <- (name, pos) :: st.ext
|
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; _ } ->
|
| 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;
|
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||||
List.iter (resolve_targ st pos) args
|
List.iter (resolve_targ st pos) args
|
||||||
| T_named { modl = None; name; args; pos } ->
|
| T_named { modl = None; name; args; pos } ->
|
||||||
@@ -124,6 +135,7 @@ let rec resolve_pattern st seen = function
|
|||||||
seen := n :: !seen;
|
seen := n :: !seen;
|
||||||
bind st pos n false))
|
bind st pos n false))
|
||||||
| P_ctor { modl = Some a; args; pos; _ } ->
|
| 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;
|
if Hashtbl.find_opt st.items a <> Some K_import then external_ref st a pos;
|
||||||
List.iter (resolve_pattern st seen) args
|
List.iter (resolve_pattern st seen) args
|
||||||
| P_ctor { modl = None; name; args; pos } ->
|
| P_ctor { modl = None; name; args; pos } ->
|
||||||
@@ -199,7 +211,12 @@ let rec resolve_expr st = function
|
|||||||
| E_call { callee; args; _ } ->
|
| E_call { callee; args; _ } ->
|
||||||
resolve_expr st callee;
|
resolve_expr st callee;
|
||||||
List.iter (resolve_expr st) args
|
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 } ->
|
| E_inst { callee; args; pos } ->
|
||||||
resolve_expr st callee;
|
resolve_expr st callee;
|
||||||
List.iter (resolve_targ st pos) args
|
List.iter (resolve_targ st pos) args
|
||||||
@@ -312,6 +329,7 @@ let resolve (m : modul) : info * error list =
|
|||||||
scopes = [];
|
scopes = [];
|
||||||
errors = [];
|
errors = [];
|
||||||
ext = [];
|
ext = [];
|
||||||
|
used = Hashtbl.create 8;
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
|
(* 1차: 모듈 수준 이름을 모은다. 선언 순서에 의존하지 않는다. *)
|
||||||
@@ -362,6 +380,17 @@ let resolve (m : modul) : info * error list =
|
|||||||
resolve_expr st value;
|
resolve_expr st value;
|
||||||
pop st)
|
pop st)
|
||||||
m.items;
|
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) =
|
let by_pos (_, a) (_, b) =
|
||||||
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
|
compare (a.Token.line, a.Token.col) (b.Token.line, b.Token.col)
|
||||||
in
|
in
|
||||||
|
|||||||
+19
-10
@@ -98,13 +98,17 @@ let read_file file =
|
|||||||
let err_of file (pos : Token.pos) msg =
|
let err_of file (pos : Token.pos) msg =
|
||||||
{ file; line = pos.line; col = pos.col; message = msg }
|
{ file; line = pos.line; col = pos.col; message = msg }
|
||||||
|
|
||||||
|
(* 파서는 항목 단위로 회복하므로 오류가 여럿일 수 있다. 첫 오류에서 멈추면
|
||||||
|
고칠 때마다 다시 돌려야 하고, 그것이 빠른 루프의 반대다. *)
|
||||||
let parse_file file =
|
let parse_file file =
|
||||||
match Lexer.lex_result (read_file file) with
|
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 -> (
|
| Ok toks -> (
|
||||||
match Parser.parse_result toks with
|
match Parser.parse_all toks with
|
||||||
| Error e -> Error (err_of file e.pos e.msg)
|
| m, [] -> Ok m
|
||||||
| Ok 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) =
|
let imports_of (m : Ast.modul) =
|
||||||
List.filter_map
|
List.filter_map
|
||||||
@@ -116,13 +120,13 @@ let imports_of (m : Ast.modul) =
|
|||||||
let check_module st path : entry =
|
let check_module st path : entry =
|
||||||
st.checked <- path :: st.checked;
|
st.checked <- path :: st.checked;
|
||||||
match parse_file path with
|
match parse_file path with
|
||||||
| Error e ->
|
| Error es ->
|
||||||
{
|
{
|
||||||
path;
|
path;
|
||||||
ast = { items = [] };
|
ast = { items = [] };
|
||||||
imports = [];
|
imports = [];
|
||||||
iface = { items = []; hash = "" };
|
iface = { items = []; hash = "" };
|
||||||
errors = [ e ];
|
errors = es;
|
||||||
}
|
}
|
||||||
| Ok ast ->
|
| Ok ast ->
|
||||||
let imports =
|
let imports =
|
||||||
@@ -145,15 +149,20 @@ let check_module st path : entry =
|
|||||||
in
|
in
|
||||||
let iface = Iface.of_module ast in
|
let iface = Iface.of_module ast in
|
||||||
let _, rerrors = Resolve.resolve ast in
|
let _, rerrors = Resolve.resolve ast in
|
||||||
|
(* 이름을 해소하지 못했으면 뒤 단계는 의미가 없다. lint는 막지 않는다. *)
|
||||||
|
let blocking =
|
||||||
|
List.filter (fun (e : Resolve.error) -> e.blocking) rerrors
|
||||||
|
in
|
||||||
let errors =
|
let errors =
|
||||||
List.map (fun (e : Resolve.error) -> err_of path e.pos e.msg) rerrors
|
List.map (fun (e : Resolve.error) -> err_of path e.pos e.msg) rerrors
|
||||||
in
|
in
|
||||||
let errors =
|
let errors =
|
||||||
if errors <> [] then errors
|
if blocking <> [] then errors
|
||||||
else
|
else
|
||||||
List.map
|
errors
|
||||||
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
|
@ List.map
|
||||||
(Typecheck.check ~imports:dep_surface ast)
|
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
|
||||||
|
(Typecheck.check ~imports:dep_surface ast)
|
||||||
@ List.map
|
@ List.map
|
||||||
(fun (e : Move.error) -> err_of path e.pos e.msg)
|
(fun (e : Move.error) -> err_of path e.pos e.msg)
|
||||||
(Move.check ~imports:dep_surface ast)
|
(Move.check ~imports:dep_surface ast)
|
||||||
|
|||||||
+39
-1
@@ -35,6 +35,9 @@ type env = {
|
|||||||
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
|
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
|
||||||
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
|
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
|
||||||
mutable performed : (T.atom * Token.pos) list;
|
mutable performed : (T.atom * Token.pos) list;
|
||||||
|
(* 이 본문에서 모르는 것을 만났는가. 과잉 선언 판정에만 쓴다 — 외부 타입의
|
||||||
|
메서드는 effect를 알 수 없으므로 "수행하지 않았다"고 말할 근거가 없다. *)
|
||||||
|
mutable saw_unknown : bool;
|
||||||
mutable errors : error list;
|
mutable errors : error list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +185,9 @@ let rec infer env (e : expr) : T.t =
|
|||||||
| None -> (
|
| None -> (
|
||||||
match Hashtbl.find_opt env.ctors n with
|
match Hashtbl.find_opt env.ctors n with
|
||||||
| Some enum -> nullary_ctor env enum n
|
| Some enum -> nullary_ctor env enum n
|
||||||
| None -> T.TUnknown)))))
|
| None ->
|
||||||
|
env.saw_unknown <- true;
|
||||||
|
T.TUnknown)))))
|
||||||
| E_list (xs, pos) ->
|
| E_list (xs, pos) ->
|
||||||
let elem = T.fresh () in
|
let elem = T.fresh () in
|
||||||
List.iter
|
List.iter
|
||||||
@@ -429,6 +434,7 @@ and infer_call env callee args pos =
|
|||||||
in
|
in
|
||||||
match fn_ty with
|
match fn_ty with
|
||||||
| None ->
|
| None ->
|
||||||
|
env.saw_unknown <- true;
|
||||||
List.iter (fun a -> ignore (infer env a)) args;
|
List.iter (fun a -> ignore (infer env a)) args;
|
||||||
T.TUnknown
|
T.TUnknown
|
||||||
| Some (T.TFn { params; eff; ret; _ }) ->
|
| Some (T.TFn { params; eff; ret; _ }) ->
|
||||||
@@ -678,6 +684,7 @@ let check_fn env (d : fn_decl) =
|
|||||||
in
|
in
|
||||||
env.ret <- declared;
|
env.ret <- declared;
|
||||||
env.performed <- [];
|
env.performed <- [];
|
||||||
|
env.saw_unknown <- false;
|
||||||
let got = infer_block env body in
|
let got = infer_block env body in
|
||||||
if not (T.unify declared got) then
|
if not (T.unify declared got) then
|
||||||
mismatch env d.fn_pos declared got
|
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)))
|
(T.atom_show m) d.fn_name (T.eff_show declared_eff)))
|
||||||
missing)
|
missing)
|
||||||
(List.rev env.performed);
|
(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 <- [];
|
env.performed <- [];
|
||||||
pop env
|
pop env
|
||||||
|
|
||||||
@@ -735,6 +772,7 @@ let check ?(imports : item list = []) (m : modul) : error list =
|
|||||||
locals = [];
|
locals = [];
|
||||||
ret = T.TUnit;
|
ret = T.TUnit;
|
||||||
performed = [];
|
performed = [];
|
||||||
|
saw_unknown = false;
|
||||||
errors = [];
|
errors = [];
|
||||||
}
|
}
|
||||||
in
|
in
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
//
|
//
|
||||||
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
|
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
|
||||||
// 이 파일은 파서가 거부해야 하는 파일이다.
|
// 이 파일은 파서가 거부해야 하는 파일이다.
|
||||||
// 아직 오류 복구가 없으므로 파서는 첫 오류에서 멈춘다 — 한 번에 하나씩 확인한다.
|
// 파서는 항목 단위로 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
|
||||||
|
// 다시 시작하므로, 한 항목에 오류 하나가 상한이다. 이 파일은 항목마다 하나씩
|
||||||
|
// 심어 회복이 실제로 되는지 본다 — 아래 넷이 모두 보고되어야 한다.
|
||||||
|
|
||||||
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
|
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
|
||||||
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
|
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
|
||||||
@@ -19,3 +21,25 @@ pub fn classify(e: PayError) -> String {
|
|||||||
_ => "other",
|
_ => "other",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [E-syntax-trailing-comma] 다중 줄 목록에는 후행 콤마가 필요하다.
|
||||||
|
// 줄바꿈이 목록을 닫으려 하면 원인을 직접 말한다.
|
||||||
|
pub fn missing_comma(
|
||||||
|
a: Int,
|
||||||
|
b: Int
|
||||||
|
) -> Int
|
||||||
|
// ERROR (parse): 다중 줄 목록에는 후행 콤마가 필요합니다
|
||||||
|
|
||||||
|
// [E-syntax-scope-parent] 자식 scope는 부모를 명시해야 한다.
|
||||||
|
// 부모 없는 작업이 생기지 않게 하는 것은 문법의 일이다.
|
||||||
|
pub fn orphan(root: TaskScope) {
|
||||||
|
scope sc {
|
||||||
|
sc.spawn(fn() { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ERROR (parse): = (자식 scope의 부모를 명시해야 합니다)
|
||||||
|
|
||||||
|
// 회복이 되었다는 증거: 이 마지막 선언은 정상적으로 읽혀야 한다.
|
||||||
|
pub fn fine(n: Int) -> Int {
|
||||||
|
n + 1
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// 13. 두 lint — 미사용 import와 effect 과잉 선언
|
||||||
|
//
|
||||||
|
// 둘 다 취향 문제가 아니라 비용 문제다.
|
||||||
|
//
|
||||||
|
// 미사용 import: 쓰지도 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
|
||||||
|
// 증분 루프의 비용을 아무 이유 없이 넓히는 선언이다.
|
||||||
|
//
|
||||||
|
// effect 과잉 선언: 선언한 effect를 수행하지 않으면 호출자는 하지도 않는
|
||||||
|
// 일에 대한 의무를 진다 — 자기 effects 절을 넓히거나 capability를 받아온다.
|
||||||
|
// 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
|
||||||
|
|
||||||
|
import "cool.dev/std/list" as List
|
||||||
|
// ERROR: List을(를) 가져왔지만 쓰지 않습니다
|
||||||
|
|
||||||
|
pub capability Db {
|
||||||
|
fn read(id: Int) effects {Db.read} -> Int
|
||||||
|
fn write(id: Int) effects {Db.write}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Db.write는 선언만 하고 수행하지 않는다.
|
||||||
|
pub fn only_reads(db: Db, id: Int) effects {Db.read, Db.write} -> Int {
|
||||||
|
db.read(id)
|
||||||
|
}
|
||||||
|
// ERROR: only_reads은(는) Db.write을(를) 선언했지만 수행하지 않습니다
|
||||||
|
|
||||||
|
// 정확히 선언하면 통과한다.
|
||||||
|
pub fn honest(db: Db, id: Int) effects {Db.read} -> Int {
|
||||||
|
db.read(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// effect 변수가 있으면 판정하지 않는다. e에 무엇이 묶일지는 호출 지점이
|
||||||
|
// 정하고, 본문만 보고는 알 수 없다 — 모르는 것을 틀렸다고 말하지 않는다.
|
||||||
|
pub fn polymorphic[e: effects](f: fn() effects e) effects e {
|
||||||
|
f()
|
||||||
|
}
|
||||||
@@ -31,6 +31,11 @@
|
|||||||
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
|
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
|
||||||
capability라 자원 타입을 정의해야 affine임이 유도된다.
|
capability라 자원 타입을 정의해야 affine임이 유도된다.
|
||||||
|
|
||||||
|
13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은
|
||||||
|
호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용
|
||||||
|
import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께
|
||||||
|
보고된다.
|
||||||
|
|
||||||
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
|
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
|
||||||
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
|
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
|
||||||
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
|
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
|
||||||
|
|||||||
+107
-29
@@ -453,14 +453,17 @@ let () =
|
|||||||
check "effect 집합의 capability도 표면에 든다"
|
check "effect 집합의 capability도 표면에 든다"
|
||||||
(resolve_ext "fn f() effects {Gw.pay}" = [ "Gw" ])
|
(resolve_ext "fn f() effects {Gw.pay}" = [ "Gw" ])
|
||||||
|
|
||||||
(* --- 샘플: 01~07은 이름 해소를 통과해야 한다 --- *)
|
(* --- 샘플: 오류 샘플을 뺀 나머지는 이름 해소를 통과해야 한다 --- *)
|
||||||
|
|
||||||
|
(* 일부러 틀린 파일들. 무엇이 틀렸는지는 각 파일의 주석에 있다. *)
|
||||||
|
let error_samples = [ "08_syntax_errors.cool"; "13_lints.cool" ]
|
||||||
|
|
||||||
let () =
|
let () =
|
||||||
let dir = "../samples" in
|
let dir = "../samples" in
|
||||||
let files =
|
let files =
|
||||||
Sys.readdir dir |> Array.to_list
|
Sys.readdir dir |> Array.to_list
|
||||||
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|
|> 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
|
|> List.sort compare
|
||||||
in
|
in
|
||||||
List.iter
|
List.iter
|
||||||
@@ -580,11 +583,17 @@ let () =
|
|||||||
Sys.readdir dir |> Array.to_list
|
Sys.readdir dir |> Array.to_list
|
||||||
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|
||||||
|> List.filter (fun f ->
|
|> List.filter (fun f ->
|
||||||
f <> "05_move_errors.cool"
|
not
|
||||||
&& f <> "08_syntax_errors.cool"
|
(List.mem f
|
||||||
&& f <> "09_type_errors.cool"
|
[
|
||||||
&& f <> "10_effect_errors.cool"
|
"05_move_errors.cool";
|
||||||
&& f <> "11_exhaustiveness.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
|
|> List.sort compare
|
||||||
in
|
in
|
||||||
List.iter
|
List.iter
|
||||||
@@ -1003,17 +1012,23 @@ let run_src src =
|
|||||||
let st = Session.create ~root:dir ~std:"../std" () in
|
let st = Session.create ~root:dir ~std:"../std" () in
|
||||||
Session.run st f
|
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 =
|
let console =
|
||||||
"import \"cool.dev/std/list\" as List\n\
|
"pub capability Console {\n\
|
||||||
import \"cool.dev/std/string\" as String\n\
|
|
||||||
import \"cool.dev/std/int\" as Int\n\n\
|
|
||||||
pub capability Console {\n\
|
|
||||||
\ fn print(s: String) effects {Console.print}\n\
|
\ fn print(s: String) effects {Console.print}\n\
|
||||||
}\n\n"
|
}\n\n"
|
||||||
|
|
||||||
let outputs src expected =
|
let outputs ?(use = []) src expected =
|
||||||
match run_src (console ^ src) with
|
match run_src (imports use ^ console ^ src) with
|
||||||
| Ok out -> out = expected
|
| Ok out -> out = expected
|
||||||
| Error e ->
|
| Error e ->
|
||||||
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e);
|
||||||
@@ -1021,13 +1036,13 @@ let outputs src expected =
|
|||||||
|
|
||||||
let () =
|
let () =
|
||||||
check "산술과 출력"
|
check "산술과 출력"
|
||||||
(outputs
|
(outputs ~use:[ "Int" ]
|
||||||
"pub fn main(c: Console) effects {Console.print} {\n\
|
"pub fn main(c: Console) effects {Console.print} {\n\
|
||||||
\ c.print(Int.show(2 + 3 * 4))\n\
|
\ c.print(Int.show(2 + 3 * 4))\n\
|
||||||
}"
|
}"
|
||||||
"14\n");
|
"14\n");
|
||||||
check "match와 생성자"
|
check "match와 생성자"
|
||||||
(outputs
|
(outputs ~use:[ "Int" ]
|
||||||
"pub enum S {\n\
|
"pub enum S {\n\
|
||||||
\ A(Int),\n\
|
\ A(Int),\n\
|
||||||
\ B,\n\
|
\ B,\n\
|
||||||
@@ -1044,7 +1059,7 @@ let () =
|
|||||||
}"
|
}"
|
||||||
"42\n0\n");
|
"42\n0\n");
|
||||||
check "mut 바인딩과 대입"
|
check "mut 바인딩과 대입"
|
||||||
(outputs
|
(outputs ~use:[ "Int" ]
|
||||||
"pub fn main(c: Console) effects {Console.print} {\n\
|
"pub fn main(c: Console) effects {Console.print} {\n\
|
||||||
\ let mut n = 1\n\
|
\ let mut n = 1\n\
|
||||||
\ n = n + 10\n\
|
\ n = n + 10\n\
|
||||||
@@ -1052,7 +1067,7 @@ let () =
|
|||||||
}"
|
}"
|
||||||
"11\n");
|
"11\n");
|
||||||
check "?는 Err에서 즉시 반환한다"
|
check "?는 Err에서 즉시 반환한다"
|
||||||
(outputs
|
(outputs ~use:[ "Int" ]
|
||||||
"pub enum E {\n\
|
"pub enum E {\n\
|
||||||
\ Bad,\n\
|
\ Bad,\n\
|
||||||
}\n\n\
|
}\n\n\
|
||||||
@@ -1076,7 +1091,7 @@ let () =
|
|||||||
}"
|
}"
|
||||||
"6\nerr\n");
|
"6\nerr\n");
|
||||||
check "클로저가 바깥 capability를 잡는다"
|
check "클로저가 바깥 capability를 잡는다"
|
||||||
(outputs
|
(outputs ~use:[ "List"; "Int" ]
|
||||||
"pub fn main(c: Console) effects {Console.print} {\n\
|
"pub fn main(c: Console) effects {Console.print} {\n\
|
||||||
\ List.each([1, 2], fn(n) {\n\
|
\ List.each([1, 2], fn(n) {\n\
|
||||||
\ c.print(Int.show(n))\n\
|
\ c.print(Int.show(n))\n\
|
||||||
@@ -1102,7 +1117,7 @@ let () =
|
|||||||
"pub capability Db {\n\
|
"pub capability Db {\n\
|
||||||
\ fn read() effects {Db.read} -> Int\n\
|
\ fn read() effects {Db.read} -> Int\n\
|
||||||
}\n\n\
|
}\n\n\
|
||||||
pub fn main(d: Db) effects {Db.read} { }"
|
pub fn main(d: Db) effects {Db.read} -> Int { d.read() }"
|
||||||
with
|
with
|
||||||
| Error e -> has_sub e.message "제공하지 않습니다"
|
| Error e -> has_sub e.message "제공하지 않습니다"
|
||||||
| Ok _ -> false)
|
| Ok _ -> false)
|
||||||
@@ -1121,23 +1136,19 @@ let () =
|
|||||||
Session.load st f;
|
Session.load st f;
|
||||||
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
|
List.map (fun (e : Session.error) -> e.message) (Session.errors st)
|
||||||
in
|
in
|
||||||
let hdr =
|
let hdr use = imports use ^ "\n" ^ console in
|
||||||
"import \"cool.dev/std/list\" as List\n\
|
let list_only = hdr [ "List" ] in
|
||||||
import \"cool.dev/std/int\" as Int\n\n\
|
let hdr = hdr [ "List"; "Int" ] in
|
||||||
pub capability Console {\n\
|
|
||||||
\ fn print(s: String) effects {Console.print}\n\
|
|
||||||
}\n\n"
|
|
||||||
in
|
|
||||||
check "std 시그니처로 인자 개수를 잡는다"
|
check "std 시그니처로 인자 개수를 잡는다"
|
||||||
(List.exists
|
(List.exists
|
||||||
(fun m -> has_sub m "인자 1개가 필요한데")
|
(fun m -> has_sub m "인자 1개가 필요한데")
|
||||||
(std_check
|
(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 시그니처로 반환 타입을 잡는다"
|
check "std 시그니처로 반환 타입을 잡는다"
|
||||||
(List.exists
|
(List.exists
|
||||||
(fun m -> has_sub m "String이(가) 필요한데 Int")
|
(fun m -> has_sub m "String이(가) 필요한데 Int")
|
||||||
(std_check
|
(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 변수가 호출 지점에서 실제로 해소된다 *)
|
(* effect 변수가 호출 지점에서 실제로 해소된다 *)
|
||||||
check "List.each의 effect 변수가 클로저의 effect로 묶인다"
|
check "List.each의 effect 변수가 클로저의 effect로 묶인다"
|
||||||
(List.exists
|
(List.exists
|
||||||
@@ -1154,3 +1165,70 @@ let () =
|
|||||||
\ List.each(xs, fn(n) { c.print(Int.show(n)) })\n\
|
\ 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