From 5593b54772a30a66c6028d7c9fb2fc9a7d8eeda7 Mon Sep 17 00:00:00 2001 From: coolguy Date: Sun, 30 Aug 2026 15:47:44 +0900 Subject: [PATCH] =?UTF-8?q?std:=20=ED=91=9C=EC=A4=80=20=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EB=B8=8C=EB=9F=AC=EB=A6=AC=20=E2=80=94=20effect=20=EB=8B=A4?= =?UTF-8?q?=ED=98=95=EC=84=B1=EC=9D=B4=20=EC=B2=98=EC=9D=8C=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B2=80=EC=82=AC=EB=90=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit std/list.cool, string.cool, int.cool, bool.cool. 본문 없는 선언이고 런타임이 구현한다. 이 파일들은 구현이 아니라 시험대다. 부채 상환이 아니라 검증이다. std가 없을 때 List.each는 모르는 이름이라 조용히 통과했다. "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만, 그 그늘에 검사되지 않는 영역이 숨어 있었다. 넣자마자 샘플 01이 깨졌다 — List.each에 Result를 반환하는 클로저를 넘기고 그 안에서 ?를 쓰고 있었다. each는 값을 남기지 않는 클로저만 받고, ?는 클로저 밖으로 나가지 못하며, 결과를 버릴 방법은 언어에 없다. map으로 고쳤다. 이것이 std를 먼저 한 이유 그 자체다. - IR은 이제 모듈 그래프 전체를 받고 전역 이름은 "<경로>#<이름>"으로 정규화된다. 별칭은 가져오는 쪽의 선택이므로 실행 의미에 남아서는 안 된다. - 본문 없는 선언은 런타임 구현으로 낮아지고, 그 이름은 모듈 파일에서 온다 (std/list.cool의 each = "list.each"). 별칭과 무관하다. - prelude 없음. std도 명시적으로 가져온다. - samples/12: effect 변수가 호출 지점에서 실제로 해소된다는 증거. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E --- docs/thesis.md | 11 ++ lib/interp.ml | 26 +-- lib/ir.ml | 227 ++++++++++++++++++--------- lib/session.ml | 76 +++++++-- samples/01_capability_signature.cool | 11 +- samples/12_stdlib_effects.cool | 33 ++++ samples/README.md | 4 + samples/run/control.cool | 2 + samples/run/hello.cool | 12 ++ std/bool.cool | 3 + std/int.cool | 4 + std/list.cool | 21 +++ std/string.cool | 4 + test/dune | 3 +- test/test_coollang.ml | 57 ++++++- 15 files changed, 386 insertions(+), 108 deletions(-) create mode 100644 samples/12_stdlib_effects.cool create mode 100644 std/bool.cool create mode 100644 std/int.cool create mode 100644 std/list.cool create mode 100644 std/string.cool diff --git a/docs/thesis.md b/docs/thesis.md index e0e8e51..0ce56b6 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -338,6 +338,17 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행 invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? / 기존 개념의 중복 표현인가? → 강한 이유 없으면 거절 +■ 표준 라이브러리 (std/) +본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다. +구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가 +List.each와 List.map에서 결정된다. 규칙이 틀렸으면 여기서 드러난다. +prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌어오지 않는다는 +규칙에 예외를 두지 않는다. +※ std를 넣자마자 샘플 01이 깨졌다. List.each에 Result를 반환하는 클로저를 + 넘기고 그 안에서 ?를 쓰고 있었다. 검사되지 않던 코드가 검사되기 시작한 + 것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다. + 결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다. + ■ 측정 (2026-08, v0 fast path) 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. cold 전체 검사 245ms diff --git a/lib/interp.ml b/lib/interp.ml index d6f4c6e..e14cfa1 100644 --- a/lib/interp.ml +++ b/lib/interp.ml @@ -104,11 +104,12 @@ let root_capability name : value option = let builtin pos name (args : value list) : value = match (name, args) with - | "String.concat", [ VStr a; VStr b ] -> VStr (a ^ b) - | "String.len", [ VStr a ] -> VInt (String.length a) - | "Int.show", [ VInt n ] -> VStr (string_of_int n) - | "Bool.show", [ VBool b ] -> VStr (if b then "true" else "false") - | "List.len", [ VList xs ] -> VInt (List.length xs) + | "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b) + | "string.len", [ VStr a ] -> VInt (String.length a) + | "int.show", [ VInt n ] -> VStr (string_of_int n) + | "int.abs", [ VInt n ] -> VInt (abs n) + | "bool.show", [ VBool b ] -> VStr (if b then "true" else "false") + | "list.len", [ VList xs ] -> VInt (List.length xs) | _ -> fail pos (Printf.sprintf "%s은(는) 런타임이 제공하지 않습니다 (표준 라이브러리가 아직 없습니다)" name) @@ -217,16 +218,16 @@ and apply st pos f args = try eval st env body with Return_exc v -> v) | VCtor (enum, name, _) -> VEnum (enum, name, args) (* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *) - | VBuiltin "List.each" -> ( + | VBuiltin "list.each" -> ( match args with | [ VList xs; f ] -> List.iter (fun x -> ignore (apply st pos f [ x ])) xs; VUnit - | _ -> fail pos "List.each는 리스트와 함수를 받습니다") - | VBuiltin "List.map" -> ( + | _ -> fail pos "list.each는 리스트와 함수를 받습니다") + | VBuiltin "list.map" -> ( match args with | [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs) - | _ -> fail pos "List.map은 리스트와 함수를 받습니다") + | _ -> fail pos "list.map은 리스트와 함수를 받습니다") | VBuiltin n -> builtin pos n args | VNative f -> f args | other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other)) @@ -312,11 +313,12 @@ and eval_binary st env op a b pos = (* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은 프로그램 안에 존재하지 않는다. *) -let run (prog : Ir.program) (main_params : (string * string) list) : - (string, Token.pos * string) result = +let run (prog : Ir.program) (entry : string) + (main_params : (string * string) list) : (string, Token.pos * string) result + = Buffer.clear out; let st = { prog } in - match Hashtbl.find_opt prog.Ir.fns "main" with + match Hashtbl.find_opt prog.Ir.fns (entry ^ "#main") with | None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다") | Some fn -> ( let args = diff --git a/lib/ir.ml b/lib/ir.ml index 9d19a18..3f7c86f 100644 --- a/lib/ir.ml +++ b/lib/ir.ml @@ -17,11 +17,14 @@ type pos = Token.pos (* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를 다시 묻지 않는다. *) +(* 전역 이름은 정규화된다: "<모듈 경로>#<이름>". 별칭은 가져오는 쪽의 선택이라 + 실행 의미에 들어와서는 안 된다 — 같은 함수가 부르는 자리마다 다른 이름이 되면 + IR은 더 이상 v1의 번역 대상이 아니다. *) type ref_kind = | R_local of string - | R_global of string (* 이 프로그램의 함수 *) - | R_ctor of string * string (* enum 이름, variant 이름 *) - | R_builtin of string (* String.concat 등 런타임 제공 *) + | R_global of string (* "<경로>#<이름>" *) + | R_ctor of string * string (* enum 이름, 정규화된 variant 이름 *) + | R_builtin of string (* 본문 없는 선언 = 런타임이 구현한다 *) type pat = | IP_wild @@ -71,7 +74,23 @@ let qual modl name = match modl with Some a -> a ^ "." ^ name | None -> name (* 지역 이름 스택. 검사 단계가 아니라 분류만 한다 — 여기서 못 찾은 이름은 전역이거나 생성자이거나 런타임 제공이다. *) -type lctx = { prog : program; mutable locals : string list list } +type modinfo = { + m_path : string; (* 정규 경로 *) + m_ast : Ast.modul; + m_deps : (string * string) list; (* 별칭 -> 정규 경로 *) +} + +(* 본문 없는 선언은 런타임이 구현한다. 그 이름은 모듈 파일 이름에서 온다: + std/list.cool의 each는 "list.each"다. 가져오는 쪽의 별칭과 무관하다. *) +let builtin_name path name = + Filename.remove_extension (Filename.basename path) ^ "." ^ name + +type lctx = { + prog : program; + mods : (string, modinfo) Hashtbl.t; + cur : modinfo; + mutable locals : string list list; +} let lpush c = c.locals <- [] :: c.locals let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> () @@ -82,41 +101,85 @@ let lbind c n = | [] -> c.locals <- [ [ n ] ] let is_local c n = List.exists (fun s -> List.mem n s) c.locals +let builtin_ctors = [ "Ok"; "Err"; "Some"; "None" ] -let builtins = - [ - "String.concat"; - "String.len"; - "Int.show"; - "Bool.show"; - "List.len"; - "List.each"; - "List.map"; - "print"; - ] +(* 한 모듈 안에서 이름 하나를 분류한다. 지역이 아니면 그 모듈의 정의를 본다. *) +let in_module c (mi : modinfo) name = + let key = mi.m_path ^ "#" ^ name in + if Hashtbl.mem c.prog.ctors key then + match Hashtbl.find_opt c.prog.ctors key with + | Some (enum, _) -> Some (R_ctor (enum, key)) + | None -> None + else if Hashtbl.mem c.prog.consts key then Some (R_global key) + else + let decl = + List.find_map + (function + | Ast.I_fn { decl; _ } when decl.fn_name = name -> Some decl + | _ -> None) + mi.m_ast.items + in + match decl with + | Some d when d.fn_body <> None -> Some (R_global key) + | Some _ -> Some (R_builtin (builtin_name mi.m_path name)) + | None -> None let classify c name = if is_local c name then R_local name + else if List.mem name builtin_ctors then + R_ctor ((if name = "Ok" || name = "Err" then "Result" else "Option"), name) else - match Hashtbl.find_opt c.prog.ctors name with - | Some (enum, _) -> R_ctor (enum, name) - | None -> - if Hashtbl.mem c.prog.fns name || Hashtbl.mem c.prog.consts name then - R_global name - else R_builtin name + match in_module c c.cur name with + | Some k -> k + | None -> ( + (* 한정 이름 "A.f": A가 이 모듈의 import면 그 모듈에서 찾는다. *) + match String.index_opt name '.' with + | None -> R_builtin name + | Some i -> ( + let a = String.sub name 0 i in + let f = String.sub name (i + 1) (String.length name - i - 1) in + match List.assoc_opt a c.cur.m_deps with + | None -> R_builtin name + | Some path -> ( + match Hashtbl.find_opt c.mods path with + | None -> R_builtin name + | Some mi -> ( + match in_module c mi f with + | Some k -> k + | None -> R_builtin (builtin_name path f))))) + +(* 패턴의 생성자도 정규 이름으로 접는다. 값 쪽과 같은 키를 써야 match가 + 성립한다 — 두 곳이 다른 규칙을 쓰면 조용히 안 맞는다. *) +let ctor_key c modl name : string option = + if List.mem name builtin_ctors then Some name + else + let path = + match modl with + | None -> Some c.cur.m_path + | Some a -> List.assoc_opt a c.cur.m_deps + in + match path with + | None -> None + | Some p -> + let k = p ^ "#" ^ name in + if Hashtbl.mem c.prog.ctors k then Some k else None let rec lower_pat c (p : Ast.pattern) : pat = match p with | Ast.P_wild _ -> IP_wild | Ast.P_lit (l, _) -> IP_lit l - | Ast.P_bind (n, _) -> + | Ast.P_bind (n, _) -> ( (* 인자 없는 생성자는 이름만 쓴다. 바인딩과 구별은 여기서 끝난다. *) - if Hashtbl.mem c.prog.ctors n then IP_ctor (n, []) - else ( - lbind c n; - IP_bind n) + match ctor_key c None n with + | Some k -> IP_ctor (k, []) + | None -> + lbind c n; + IP_bind n) | Ast.P_ctor { modl; name; args; _ } -> - IP_ctor (qual modl name, List.map (lower_pat c) args) + let k = + match ctor_key c modl name with Some k -> k | None -> qual modl name + in + IP_ctor (k, List.map (lower_pat c) args) let rec lower c (e : Ast.expr) : t = match e with @@ -238,35 +301,43 @@ and lower_stmt c (s : Ast.stmt) : stmt = S_assign { place = lower c place; value = lower c value; pos } | Ast.S_expr e -> S_do (lower c e) -let of_module ?(imports : Ast.item list = []) (m : Ast.modul) : program = +(* 모듈 그래프 전체를 하나의 IR 프로그램으로 낮춘다. + + 모듈 하나씩 낮추고 싶은 유혹이 있지만, 그러면 별칭이 실행 의미에 남는다. + 같은 함수가 A에서는 Shapes.double, B에서는 Geo.double이 되어 IR이 더 이상 + 프로그램의 정규형이 아니게 된다. 정규 경로로 한 번 접는다. *) +let of_program (mods : modinfo list) : program = let prog = { - fns = Hashtbl.create 32; - ctors = Hashtbl.create 32; - caps = Hashtbl.create 8; - consts = Hashtbl.create 8; + fns = Hashtbl.create 64; + ctors = Hashtbl.create 64; + caps = Hashtbl.create 16; + consts = Hashtbl.create 16; } in - let items = imports @ m.items in - (* 1차: 이름부터. 낮추기가 이름을 분류하려면 전체가 먼저 보여야 한다. *) + let index = Hashtbl.create 16 in + List.iter (fun mi -> Hashtbl.replace index mi.m_path mi) mods; + (* 1차: 이름부터. 낮추기가 이름을 분류하려면 그래프 전체가 먼저 보여야 한다. *) List.iter - (fun (it : Ast.item) -> - match it with - | Ast.I_enum { name; variants; _ } -> - List.iter - (fun (v : Ast.variant) -> - Hashtbl.replace prog.ctors v.v_name (name, List.length v.v_args)) - variants - | Ast.I_capability { name; methods; _ } -> - Hashtbl.replace prog.caps name - (List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods) - | Ast.I_fn { decl; _ } when decl.fn_body <> None -> - Hashtbl.replace prog.fns decl.fn_name - { fn_name = decl.fn_name; fn_params = []; fn_body = I_unit } - | Ast.I_const { name; _ } -> Hashtbl.replace prog.consts name I_unit - | _ -> ()) - items; - (* builtin 생성자도 IR이 알아야 한다 *) + (fun mi -> + let key n = mi.m_path ^ "#" ^ n in + List.iter + (fun (it : Ast.item) -> + match it with + | Ast.I_enum { name; variants; _ } -> + List.iter + (fun (v : Ast.variant) -> + Hashtbl.replace prog.ctors (key v.v_name) + (name, List.length v.v_args)) + variants + | Ast.I_capability { name; methods; _ } -> + Hashtbl.replace prog.caps name + (List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods) + | Ast.I_const { name; _ } -> + Hashtbl.replace prog.consts (key name) I_unit + | _ -> ()) + mi.m_ast.items) + mods; List.iter (fun (v, e, n) -> Hashtbl.replace prog.ctors v (e, n)) [ @@ -276,29 +347,37 @@ let of_module ?(imports : Ast.item list = []) (m : Ast.modul) : program = ("None", "Option", 0); ]; (* 2차: 본문 *) - let c = { prog; locals = [] } in List.iter - (fun (it : Ast.item) -> - match it with - | Ast.I_fn { decl; _ } -> ( - match decl.fn_body with - | None -> () - | Some body -> + (fun mi -> + let c = { prog; mods = index; cur = mi; locals = [] } in + let key n = mi.m_path ^ "#" ^ n in + List.iter + (fun (it : Ast.item) -> + match it with + | Ast.I_fn { decl; _ } -> ( + match decl.fn_body with + | None -> () + | Some body -> + c.locals <- []; + lpush c; + List.iter + (fun (p : Ast.param) -> lbind c p.p_name) + decl.fn_params; + let b = lower_block c body in + lpop c; + Hashtbl.replace prog.fns (key decl.fn_name) + { + fn_name = key decl.fn_name; + fn_params = + List.map + (fun (p : Ast.param) -> p.p_name) + decl.fn_params; + fn_body = b; + }) + | Ast.I_const { name; value; _ } -> c.locals <- []; - lpush c; - List.iter (fun (p : Ast.param) -> lbind c p.p_name) decl.fn_params; - let b = lower_block c body in - lpop c; - Hashtbl.replace prog.fns decl.fn_name - { - fn_name = decl.fn_name; - fn_params = - List.map (fun (p : Ast.param) -> p.p_name) decl.fn_params; - fn_body = b; - }) - | Ast.I_const { name; value; _ } -> - c.locals <- []; - Hashtbl.replace prog.consts name (lower c value) - | _ -> ()) - items; + Hashtbl.replace prog.consts (key name) (lower c value) + | _ -> ()) + mi.m_ast.items) + mods; prog diff --git a/lib/session.ml b/lib/session.ml index 145e48b..dc9d264 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -26,13 +26,40 @@ type entry = { type t = { root : string; + std : string option; (* 표준 라이브러리 디렉터리 *) modules : (string, entry) Hashtbl.t; (* 통계: 무엇이 몇 번 재검사됐는지. 측정이 목적이므로 처음부터 센다. *) mutable checked : string list; } -let create ?(root = ".") () = - { root; modules = Hashtbl.create 16; checked = [] } +(* 표준 라이브러리를 찾는다. COOL_STD가 있으면 그것, 없으면 위로 올라가며 + std/list.cool을 찾는다. 못 찾으면 없는 것이고, 그때 std 이름들은 외부 + 참조로 남는다 — 없다고 말하지 않는다. *) +let find_std root = + match Sys.getenv_opt "COOL_STD" with + | Some d when Sys.file_exists d -> Some d + | _ -> + let rec up dir n = + if n = 0 then None + else + let cand = Filename.concat dir "std" in + if Sys.file_exists (Filename.concat cand "list.cool") then Some cand + else + let parent = Filename.dirname dir in + if parent = dir then None else up parent (n - 1) + in + up + (if Filename.is_relative root then Filename.concat (Sys.getcwd ()) root + else root) + 8 + +let create ?(root = ".") ?std () = + { + root; + std = (match std with Some _ -> std | None -> find_std root); + modules = Hashtbl.create 16; + checked = []; + } (* 패키지 경로와 지역 모듈 경로를 구분한다. 첫 세그먼트에 점이 있으면 패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런 @@ -42,7 +69,24 @@ let is_package path = | Some i -> String.contains (String.sub path 0 i) '.' | None -> String.contains path '.' -let resolve_import st path = Filename.concat st.root (path ^ ".cool") +let std_prefix = "cool.dev/std/" + +let starts_with p s = + String.length s >= String.length p && String.sub s 0 (String.length p) = p + +(* 경로를 파일로 바꾼다. 바꿀 수 없으면 None — v0에는 패키지 해소가 없으므로 + 표준 라이브러리 밖의 패키지는 불투명하게 남는다. *) +let resolve_import st path : string option = + if starts_with std_prefix path then + let name = + String.sub path (String.length std_prefix) + (String.length path - String.length std_prefix) + in + match st.std with + | Some d -> Some (Filename.concat d (name ^ ".cool")) + | None -> None + else if is_package path then None + else Some (Filename.concat st.root (path ^ ".cool")) let read_file file = let ic = open_in_bin file in @@ -84,7 +128,9 @@ let check_module st path : entry = let imports = List.filter_map (fun (a, p) -> - if is_package p then None else Some (a, resolve_import st p)) + match resolve_import st p with + | Some f -> Some (a, f) + | None -> None) (imports_of ast) in (* 의존 모듈의 exported surface를 소비 측 별칭으로 한정해 합친다. @@ -137,8 +183,9 @@ let rec load st ?(visiting = []) path : unit = | Ok ast -> List.iter (fun (_, p) -> - if not (is_package p) then - load st ~visiting:(path :: visiting) (resolve_import st p)) + match resolve_import st p with + | Some f -> load st ~visiting:(path :: visiting) f + | None -> ()) (imports_of ast)); Hashtbl.replace st.modules path (check_module st path) end @@ -211,15 +258,14 @@ let run st path : (string, error) result = | None -> Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } | Some e -> ( - let dep_surface = - List.concat_map - (fun (alias, p) -> - match Hashtbl.find_opt st.modules p with - | Some d -> Iface.qualify alias d.iface.Iface.items - | None -> []) - e.imports + (* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *) + let mods = + Hashtbl.fold + (fun p (d : entry) acc -> + { Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc) + st.modules [] in - let prog = Ir.of_module ~imports:dep_surface e.ast in - match Interp.run prog (main_params e.ast) with + let prog = Ir.of_program mods in + match Interp.run prog path (main_params e.ast) with | Ok out -> Ok out | Error (pos, msg) -> Error (err_of path pos msg)) diff --git a/samples/01_capability_signature.cool b/samples/01_capability_signature.cool index ed204c9..316a3c8 100644 --- a/samples/01_capability_signature.cool +++ b/samples/01_capability_signature.cool @@ -43,12 +43,15 @@ pub fn checkout( } // 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다). +// +// each가 아니라 map인 이유: 결과를 버릴 방법이 언어에 없다. each는 값을 +// 남기지 않는 클로저만 받으므로 Result를 삼킬 수 없고, ?는 클로저 밖으로 +// 나가지 못한다. 실패를 못 본 척하려면 명시적으로 match해야 한다. pub fn refund_all( pay: PaymentGateway, ids: List[OrderId], -) effects {PaymentGateway.refund} -> Result[Unit, PayError] { - List.each(ids, fn(id) { - refund_order(pay, id)? - Ok(unit) +) effects {PaymentGateway.refund} -> List[Result[Receipt, PayError]] { + List.map(ids, fn(id) { + refund_order(pay, id) }) } diff --git a/samples/12_stdlib_effects.cool b/samples/12_stdlib_effects.cool new file mode 100644 index 0000000..0f11568 --- /dev/null +++ b/samples/12_stdlib_effects.cool @@ -0,0 +1,33 @@ +// 12. 표준 라이브러리 시그니처가 실제로 검사된다 +// +// std가 생기기 전에는 List.each가 모르는 이름이라 조용히 통과했다. +// "모르는 것을 틀렸다고 말하지 않는다"는 맞는 원칙이지만, 그 그늘에 +// 검사되지 않는 영역이 숨어 있었다. 이제 그늘이 없다. +// +// 이 파일은 세 가지 오류를 낸다. + +import "cool.dev/std/list" as List +import "cool.dev/std/int" as Int + +pub capability Console { + fn print(s: String) effects {Console.print} +} + +// (1) effect 다형성. e는 클로저의 시그니처에서 {Console.print}로 묶이고, +// 그것이 이 함수의 effects 절 {}를 넘는다. effect 변수가 호출 지점에서 +// 실제로 해소된다는 증거다 — 이게 안 걸리면 규칙이 장식이다. +pub fn leaks_effect(c: Console, xs: List[Int]) { + List.each(xs, fn(n) { + c.print(Int.show(n)) + }) +} + +// (2) 인자 개수 +pub fn wrong_arity(xs: List[Int]) -> Int { + List.len(xs, 1) +} + +// (3) 반환 타입 +pub fn wrong_type(xs: List[Int]) -> String { + List.len(xs) +} diff --git a/samples/README.md b/samples/README.md index 9e0b6c7..b266eb0 100644 --- a/samples/README.md +++ b/samples/README.md @@ -31,6 +31,10 @@ capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가 capability라 자원 타입을 정의해야 affine임이 유도된다. +12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는 +`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지 +않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다. + 01~04, 06, 07은 `coolc check`를 통과한다 (exit 0). `modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라 diff --git a/samples/run/control.cool b/samples/run/control.cool index 615ee5f..a1ae15b 100644 --- a/samples/run/control.cool +++ b/samples/run/control.cool @@ -6,6 +6,8 @@ // err // in scope +import "cool.dev/std/int" as Int + pub capability Console { fn print(s: String) effects {Console.print} } diff --git a/samples/run/hello.cool b/samples/run/hello.cool index 92a1ca3..fb90254 100644 --- a/samples/run/hello.cool +++ b/samples/run/hello.cool @@ -3,6 +3,18 @@ // main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면 // print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의 // 실행 시점 의미다. +// +// 표준 라이브러리도 명시적으로 가져온다. prelude가 없다 — +// 암묵적으로 끌어오지 않는다는 규칙에 예외를 두지 않는다. +// +// 기대 출력: +// area = 12 +// area = 9 +// area = 3 + +import "cool.dev/std/list" as List +import "cool.dev/std/string" as String +import "cool.dev/std/int" as Int pub capability Console { fn print(s: String) effects {Console.print} diff --git a/std/bool.cool b/std/bool.cool new file mode 100644 index 0000000..8f20ca8 --- /dev/null +++ b/std/bool.cool @@ -0,0 +1,3 @@ +// 표준 라이브러리: 불리언. + +pub fn show(b: Bool) -> String diff --git a/std/int.cool b/std/int.cool new file mode 100644 index 0000000..9ea6ec2 --- /dev/null +++ b/std/int.cool @@ -0,0 +1,4 @@ +// 표준 라이브러리: 정수. + +pub fn show(n: Int) -> String +pub fn abs(n: Int) -> Int diff --git a/std/list.cool b/std/list.cool new file mode 100644 index 0000000..7acae52 --- /dev/null +++ b/std/list.cool @@ -0,0 +1,21 @@ +// 표준 라이브러리: 리스트. +// +// 본문이 없다. 런타임이 구현하고, 이 파일은 그 계약을 말한다. +// 그래서 이 파일은 구현이 아니라 시험대다 — effect 다형성이 실제로 쓸 만한지가 +// 여기서 결정된다. each와 map이 effect 변수 하나로 표현되지 않으면 규칙이 +// 틀린 것이고, 그건 v1로 미룰 수 없는 발견이다. +// +// e는 파라미터의 effect 슬롯에 홀로 나타난다 (결정 위치). 호출 지점에서 +// 인자의 시그니처를 읽어 묶인다 — 추론이 아니라 읽기다. + +pub fn len[a](xs: List[a]) -> Int + +pub fn each[a, e: effects]( + xs: List[a], + f: fn(a) effects e, +) effects e + +pub fn map[a, b, e: effects]( + xs: List[a], + f: fn(a) effects e -> b, +) effects e -> List[b] diff --git a/std/string.cool b/std/string.cool new file mode 100644 index 0000000..a836b62 --- /dev/null +++ b/std/string.cool @@ -0,0 +1,4 @@ +// 표준 라이브러리: 문자열. + +pub fn len(s: String) -> Int +pub fn concat(a: String, b: String) -> String diff --git a/test/dune b/test/dune index 38438f3..bac39b0 100644 --- a/test/dune +++ b/test/dune @@ -2,4 +2,5 @@ (name test_coollang) (libraries coollang) (deps - (glob_files %{workspace_root}/samples/*.cool))) + (glob_files %{workspace_root}/samples/*.cool) + (glob_files %{workspace_root}/std/*.cool))) diff --git a/test/test_coollang.ml b/test/test_coollang.ml index 7059a5b..28974ad 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -999,11 +999,16 @@ let run_src src = 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 () in + (* 표준 라이브러리는 저장소의 std/. 테스트는 /tmp에서 도니 명시한다. *) + let st = Session.create ~root:dir ~std:"../std" () in Session.run st f +(* prelude가 없다. 표준 라이브러리도 명시적으로 가져온다. *) let console = - "pub capability Console {\n\ + "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\ \ fn print(s: String) effects {Console.print}\n\ }\n\n" @@ -1101,3 +1106,51 @@ let () = with | Error e -> has_sub e.message "제공하지 않습니다" | Ok _ -> false) + +(* 표준 라이브러리 시그니처가 실제 검사에 쓰이는지. + + std가 생기기 전에는 List.each가 모르는 이름이라 조용히 통과했다. + effect 다형성이 장식이 아니라는 것을 여기서 고정한다. *) +let () = + let std_check src = + let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_stdtest" 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 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 + 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}"))); + 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}"))); + (* effect 변수가 호출 지점에서 실제로 해소된다 *) + check "List.each의 effect 변수가 클로저의 effect로 묶인다" + (List.exists + (fun m -> has_sub m "선언되지 않은 effect Console.print") + (std_check + (hdr + ^ "pub fn f(c: Console, xs: List[Int]) {\n\ + \ List.each(xs, fn(n) { c.print(Int.show(n)) })\n\ + }"))); + check "effect를 선언하면 같은 코드가 통과한다" + (std_check + (hdr + ^ "pub fn f(c: Console, xs: List[Int]) effects {Console.print} {\n\ + \ List.each(xs, fn(n) { c.print(Int.show(n)) })\n\ + }") + = [])