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
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+25 -1
View File
@@ -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
}
+35
View File
@@ -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()
}
+5
View File
@@ -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
View File
@@ -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)