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 의미를 바꾸는가? /
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
■ lint (오류다, 경고가 아니다)
- 미사용 import: 쓰지 않는 모듈의 시그니처가 바뀌면 이 모듈이 재검사된다.
증분 루프의 비용을 이유 없이 넓히는 선언은 남겨둘 수 없다.
- effect 과잉 선언: 선언하고 수행하지 않으면 호출자가 하지도 않는 일에
의무를 진다. 시그니처는 실제보다 좁아도 안 되고 넓어도 안 된다.
단, effect 변수가 있거나 본문에 모르는 이름이 있으면 판정하지 않는다 —
무엇이 묶일지는 호출 지점이 정하고, 외부 타입의 메서드는 effect를 모른다.
lint는 blocking이 아니다. 이름 해소 실패는 뒤 단계를 막지만 lint는 막지
않는다 — lint 하나가 진짜 타입 오류를 가리면 루프가 느려진다.
■ 파서 오류 복구
항목 단위로만 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
다시 시작한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고,
틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은
정직한 한계이지 숨길 것이 아니다.
동기화 지점: 중괄호 깊이 0 + 줄 첫머리 + 선언 시작 토큰. 셋 다 필요하다.
■ 표준 라이브러리 (std/)
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
+63 -4
View File
@@ -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
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
View File
@@ -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
+17 -8
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,13 +149,18 @@ 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
errors
@ List.map
(fun (e : Typecheck.error) -> err_of path e.pos e.msg)
(Typecheck.check ~imports:dep_surface ast)
@ List.map
+39 -1
View File
@@ -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
+25 -1
View File
@@ -2,7 +2,9 @@
//
// 05와 목적이 다르다. 05는 구문은 맞지만 검사기가 거부해야 하는 파일이고,
// 이 파일은 파서가 거부해야 하는 파일이다.
// 아직 오류 복구가 없으므로 파서는 첫 오류에서 멈춘다 — 한 번에 하나씩 확인한다.
// 파서는 항목 단위로 회복한다. 오류가 난 선언은 통째로 버리고 다음 선언에서
// 다시 시작하므로, 한 항목에 오류 하나가 상한이다. 이 파일은 항목마다 하나씩
// 심어 회복이 실제로 되는지 본다 — 아래 넷이 모두 보고되어야 한다.
// [E-syntax-effect-union] 파라미터 위치의 합집합은 문법에 존재하지 않는다.
// 검사기가 아니라 파서가 거부한다 (eff_param 프로덕션에 "|"가 없다).
@@ -19,3 +21,25 @@ pub fn classify(e: PayError) -> String {
_ => "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라 자원 타입을 정의해야 affine임이 유도된다.
13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은
호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용
import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께
보고된다.
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
+107 -29
View File
@@ -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)