From 91f3840d196b1f1fcbac961fbbdba9721a3b1602 Mon Sep 17 00:00:00 2001 From: coolguy Date: Sun, 30 Aug 2026 15:53:35 +0900 Subject: [PATCH] =?UTF-8?q?lint:=20=ED=8C=8C=EC=84=9C=20=EC=98=A4=EB=A5=98?= =?UTF-8?q?=20=EB=B3=B5=EA=B5=AC=EC=99=80=20=EB=91=90=20lint=20=E2=80=94?= =?UTF-8?q?=20=EB=82=A8=EC=9D=80=20=EB=B6=80=EC=B1=84=EB=A5=BC=20=ED=84=B4?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 파서 오류 복구: 항목 단위로만 회복한다. 문 단위로 더 잘게 회복하려 하면 파서가 추측을 하게 되고, 틀린 추측은 없는 오류를 지어낸다. 한 항목에 오류 하나가 상한이라는 것은 정직한 한계다. 동기화 지점은 중괄호 깊이 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) Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E --- docs/thesis.md | 17 +++++ lib/parser.ml | 71 ++++++++++++++++-- lib/resolve.ml | 35 ++++++++- lib/session.ml | 29 +++++--- lib/typecheck.ml | 40 +++++++++- samples/08_syntax_errors.cool | 26 ++++++- samples/13_lints.cool | 35 +++++++++ samples/README.md | 5 ++ test/test_coollang.ml | 136 ++++++++++++++++++++++++++-------- 9 files changed, 344 insertions(+), 50 deletions(-) create mode 100644 samples/13_lints.cool diff --git a/docs/thesis.md b/docs/thesis.md index 0ce56b6..032ecb1 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -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 다형성이 실제로 쓸 만한지가 diff --git a/lib/parser.ml b/lib/parser.ml index aea7d2c..6481dfa 100644 --- a/lib/parser.ml +++ b/lib/parser.ml @@ -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 diff --git a/lib/resolve.ml b/lib/resolve.ml index cb29e34..3695d7d 100644 --- a/lib/resolve.ml +++ b/lib/resolve.ml @@ -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 diff --git a/lib/session.ml b/lib/session.ml index dc9d264..480a009 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -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) diff --git a/lib/typecheck.ml b/lib/typecheck.ml index 3edf47a..43a22e8 100644 --- a/lib/typecheck.ml +++ b/lib/typecheck.ml @@ -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 diff --git a/samples/08_syntax_errors.cool b/samples/08_syntax_errors.cool index 793a463..bb52ebd 100644 --- a/samples/08_syntax_errors.cool +++ b/samples/08_syntax_errors.cool @@ -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 +} diff --git a/samples/13_lints.cool b/samples/13_lints.cool new file mode 100644 index 0000000..9f73c01 --- /dev/null +++ b/samples/13_lints.cool @@ -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() +} diff --git a/samples/README.md b/samples/README.md index b266eb0..54cf354 100644 --- a/samples/README.md +++ b/samples/README.md @@ -31,6 +31,11 @@ capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가 capability라 자원 타입을 정의해야 affine임이 유도된다. +13은 두 lint다. 미사용 import는 재검사 범위를 넓히고, effect 과잉 선언은 +호출자에게 없는 의무를 지운다 — 둘 다 취향이 아니라 비용이다. 미사용 +import는 lint이므로 뒤 단계를 막지 않는다: 같은 파일의 타입 오류가 함께 +보고된다. + 12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는 `List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다. diff --git a/test/test_coollang.ml b/test/test_coollang.ml index 28974ad..f5f10f3 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -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)