std: 표준 라이브러리 — effect 다형성이 처음으로 검사된다

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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 15:47:44 +09:00
co-authored by Claude Opus 5
parent 1c4f46e5e5
commit 5593b54772
15 changed files with 386 additions and 108 deletions
+11
View File
@@ -338,6 +338,17 @@ L2 빠른 테스트 / L3 fuzzing / L4 formal proof → 요청 시, 분리 실행
invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? / invalidation 범위를 넓히는가? / unrelated code 의미를 바꾸는가? /
기존 개념의 중복 표현인가? → 강한 이유 없으면 거절 기존 개념의 중복 표현인가? → 강한 이유 없으면 거절
■ 표준 라이브러리 (std/)
본문 없는 선언 파일이다. 런타임이 구현하고 .cool 파일은 계약만 말한다.
구현이 아니라 시험대인 것이 요점 — effect 다형성이 실제로 쓸 만한지가
List.each와 List.map에서 결정된다. 규칙이 틀렸으면 여기서 드러난다.
prelude는 없다. std도 명시적으로 가져온다 — 암묵적으로 끌어오지 않는다는
규칙에 예외를 두지 않는다.
※ std를 넣자마자 샘플 01이 깨졌다. List.each에 Result를 반환하는 클로저를
넘기고 그 안에서 ?를 쓰고 있었다. 검사되지 않던 코드가 검사되기 시작한
것이고, 이것이 std를 "부채 상환"이 아니라 "검증"으로 본 이유다.
결과를 버릴 방법이 언어에 없다는 성질도 여기서 처음 확인됐다.
■ 측정 (2026-08, v0 fast path) ■ 측정 (2026-08, v0 fast path)
100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현. 100,391줄 / 200 모듈 (사슬 의존). bench/bench.ml로 재현.
cold 전체 검사 245ms cold 전체 검사 245ms
+14 -12
View File
@@ -104,11 +104,12 @@ let root_capability name : value option =
let builtin pos name (args : value list) : value = let builtin pos name (args : value list) : value =
match (name, args) with match (name, args) with
| "String.concat", [ VStr a; VStr b ] -> VStr (a ^ b) | "string.concat", [ VStr a; VStr b ] -> VStr (a ^ b)
| "String.len", [ VStr a ] -> VInt (String.length a) | "string.len", [ VStr a ] -> VInt (String.length a)
| "Int.show", [ VInt n ] -> VStr (string_of_int n) | "int.show", [ VInt n ] -> VStr (string_of_int n)
| "Bool.show", [ VBool b ] -> VStr (if b then "true" else "false") | "int.abs", [ VInt n ] -> VInt (abs n)
| "List.len", [ VList xs ] -> VInt (List.length xs) | "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) 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) try eval st env body with Return_exc v -> v)
| VCtor (enum, name, _) -> VEnum (enum, name, args) | VCtor (enum, name, _) -> VEnum (enum, name, args)
(* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *) (* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *)
| VBuiltin "List.each" -> ( | VBuiltin "list.each" -> (
match args with match args with
| [ VList xs; f ] -> | [ VList xs; f ] ->
List.iter (fun x -> ignore (apply st pos f [ x ])) xs; List.iter (fun x -> ignore (apply st pos f [ x ])) xs;
VUnit VUnit
| _ -> fail pos "List.each는 리스트와 함수를 받습니다") | _ -> fail pos "list.each는 리스트와 함수를 받습니다")
| VBuiltin "List.map" -> ( | VBuiltin "list.map" -> (
match args with match args with
| [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs) | [ 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 | VBuiltin n -> builtin pos n args
| VNative f -> f args | VNative f -> f args
| other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other)) | other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other))
@@ -312,11 +313,12 @@ and eval_binary st env op a b pos =
(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은 (* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은
프로그램 안에 존재하지 않는다. *) 프로그램 안에 존재하지 않는다. *)
let run (prog : Ir.program) (main_params : (string * string) list) : let run (prog : Ir.program) (entry : string)
(string, Token.pos * string) result = (main_params : (string * string) list) : (string, Token.pos * string) result
=
Buffer.clear out; Buffer.clear out;
let st = { prog } in 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 함수가 없습니다") | None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다")
| Some fn -> ( | Some fn -> (
let args = let args =
+153 -74
View File
@@ -17,11 +17,14 @@ type pos = Token.pos
(* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를 (* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를
다시 묻지 않는다. *) 다시 묻지 않는다. *)
(* 전역 이름은 정규화된다: "<모듈 경로>#<이름>". 별칭은 가져오는 쪽의 선택이라
실행 의미에 들어와서는 안 된다 — 같은 함수가 부르는 자리마다 다른 이름이 되면
IR은 더 이상 v1의 번역 대상이 아니다. *)
type ref_kind = type ref_kind =
| R_local of string | R_local of string
| R_global of string (* 이 프로그램의 함수 *) | R_global of string (* "<경로>#<이름>" *)
| R_ctor of string * string (* enum 이름, variant 이름 *) | R_ctor of string * string (* enum 이름, 정규화된 variant 이름 *)
| R_builtin of string (* String.concat 등 런타임 제공 *) | R_builtin of string (* 본문 없는 선언 = 런타임이 구현한다 *)
type pat = type pat =
| IP_wild | 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 lpush c = c.locals <- [] :: c.locals
let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> () let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> ()
@@ -82,41 +101,85 @@ let lbind c n =
| [] -> c.locals <- [ [ n ] ] | [] -> c.locals <- [ [ n ] ]
let is_local c n = List.exists (fun s -> List.mem n s) c.locals let is_local c n = List.exists (fun s -> List.mem n s) c.locals
let builtin_ctors = [ "Ok"; "Err"; "Some"; "None" ]
let builtins = (* 한 모듈 안에서 이름 하나를 분류한다. 지역이 아니면 그 모듈의 정의를 본다. *)
[ let in_module c (mi : modinfo) name =
"String.concat"; let key = mi.m_path ^ "#" ^ name in
"String.len"; if Hashtbl.mem c.prog.ctors key then
"Int.show"; match Hashtbl.find_opt c.prog.ctors key with
"Bool.show"; | Some (enum, _) -> Some (R_ctor (enum, key))
"List.len"; | None -> None
"List.each"; else if Hashtbl.mem c.prog.consts key then Some (R_global key)
"List.map"; else
"print"; 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 = let classify c name =
if is_local c name then R_local 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 else
match Hashtbl.find_opt c.prog.ctors name with match in_module c c.cur name with
| Some (enum, _) -> R_ctor (enum, name) | Some k -> k
| None -> | None -> (
if Hashtbl.mem c.prog.fns name || Hashtbl.mem c.prog.consts name then (* 한정 이름 "A.f": A가 이 모듈의 import면 그 모듈에서 찾는다. *)
R_global name match String.index_opt name '.' with
else R_builtin name | 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 = let rec lower_pat c (p : Ast.pattern) : pat =
match p with match p with
| Ast.P_wild _ -> IP_wild | Ast.P_wild _ -> IP_wild
| Ast.P_lit (l, _) -> IP_lit l | 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, []) match ctor_key c None n with
else ( | Some k -> IP_ctor (k, [])
lbind c n; | None ->
IP_bind n) lbind c n;
IP_bind n)
| Ast.P_ctor { modl; name; args; _ } -> | 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 = let rec lower c (e : Ast.expr) : t =
match e with 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 } S_assign { place = lower c place; value = lower c value; pos }
| Ast.S_expr e -> S_do (lower c e) | 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 = let prog =
{ {
fns = Hashtbl.create 32; fns = Hashtbl.create 64;
ctors = Hashtbl.create 32; ctors = Hashtbl.create 64;
caps = Hashtbl.create 8; caps = Hashtbl.create 16;
consts = Hashtbl.create 8; consts = Hashtbl.create 16;
} }
in in
let items = imports @ m.items in let index = Hashtbl.create 16 in
(* 1차: 이름부터. 낮추기가 이름을 분류하려면 전체가 먼저 보여야 한다. *) List.iter (fun mi -> Hashtbl.replace index mi.m_path mi) mods;
(* 1차: 이름부터. 낮추기가 이름을 분류하려면 그래프 전체가 먼저 보여야 한다. *)
List.iter List.iter
(fun (it : Ast.item) -> (fun mi ->
match it with let key n = mi.m_path ^ "#" ^ n in
| Ast.I_enum { name; variants; _ } -> List.iter
List.iter (fun (it : Ast.item) ->
(fun (v : Ast.variant) -> match it with
Hashtbl.replace prog.ctors v.v_name (name, List.length v.v_args)) | Ast.I_enum { name; variants; _ } ->
variants List.iter
| Ast.I_capability { name; methods; _ } -> (fun (v : Ast.variant) ->
Hashtbl.replace prog.caps name Hashtbl.replace prog.ctors (key v.v_name)
(List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods) (name, List.length v.v_args))
| Ast.I_fn { decl; _ } when decl.fn_body <> None -> variants
Hashtbl.replace prog.fns decl.fn_name | Ast.I_capability { name; methods; _ } ->
{ fn_name = decl.fn_name; fn_params = []; fn_body = I_unit } Hashtbl.replace prog.caps name
| Ast.I_const { name; _ } -> Hashtbl.replace prog.consts name I_unit (List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods)
| _ -> ()) | Ast.I_const { name; _ } ->
items; Hashtbl.replace prog.consts (key name) I_unit
(* builtin 생성자도 IR이 알아야 한다 *) | _ -> ())
mi.m_ast.items)
mods;
List.iter List.iter
(fun (v, e, n) -> Hashtbl.replace prog.ctors v (e, n)) (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); ("None", "Option", 0);
]; ];
(* 2차: 본문 *) (* 2차: 본문 *)
let c = { prog; locals = [] } in
List.iter List.iter
(fun (it : Ast.item) -> (fun mi ->
match it with let c = { prog; mods = index; cur = mi; locals = [] } in
| Ast.I_fn { decl; _ } -> ( let key n = mi.m_path ^ "#" ^ n in
match decl.fn_body with List.iter
| None -> () (fun (it : Ast.item) ->
| Some body -> 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 <- []; c.locals <- [];
lpush c; Hashtbl.replace prog.consts (key name) (lower c value)
List.iter (fun (p : Ast.param) -> lbind c p.p_name) decl.fn_params; | _ -> ())
let b = lower_block c body in mi.m_ast.items)
lpop c; mods;
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;
prog prog
+61 -15
View File
@@ -26,13 +26,40 @@ type entry = {
type t = { type t = {
root : string; root : string;
std : string option; (* 표준 라이브러리 디렉터리 *)
modules : (string, entry) Hashtbl.t; modules : (string, entry) Hashtbl.t;
(* 통계: 무엇이 몇 번 재검사됐는지. 측정이 목적이므로 처음부터 센다. *) (* 통계: 무엇이 몇 번 재검사됐는지. 측정이 목적이므로 처음부터 센다. *)
mutable checked : string list; mutable checked : string list;
} }
let create ?(root = ".") () = (* 표준 라이브러리를 찾는다. COOL_STD가 있으면 그것, 없으면 위로 올라가며
{ root; modules = Hashtbl.create 16; checked = [] } 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에는 패키지 해소가 없으므로 그런 패키지 참조다 (cool.dev/std/list). v0에는 패키지 해소가 없으므로 그런
@@ -42,7 +69,24 @@ let is_package path =
| Some i -> String.contains (String.sub path 0 i) '.' | Some i -> String.contains (String.sub path 0 i) '.'
| None -> String.contains path '.' | 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 read_file file =
let ic = open_in_bin file in let ic = open_in_bin file in
@@ -84,7 +128,9 @@ let check_module st path : entry =
let imports = let imports =
List.filter_map List.filter_map
(fun (a, p) -> (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) (imports_of ast)
in in
(* 의존 모듈의 exported surface를 소비 측 별칭으로 한정해 합친다. (* 의존 모듈의 exported surface를 소비 측 별칭으로 한정해 합친다.
@@ -137,8 +183,9 @@ let rec load st ?(visiting = []) path : unit =
| Ok ast -> | Ok ast ->
List.iter List.iter
(fun (_, p) -> (fun (_, p) ->
if not (is_package p) then match resolve_import st p with
load st ~visiting:(path :: visiting) (resolve_import st p)) | Some f -> load st ~visiting:(path :: visiting) f
| None -> ())
(imports_of ast)); (imports_of ast));
Hashtbl.replace st.modules path (check_module st path) Hashtbl.replace st.modules path (check_module st path)
end end
@@ -211,15 +258,14 @@ let run st path : (string, error) result =
| None -> | None ->
Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" }
| Some e -> ( | Some e -> (
let dep_surface = (* IR은 그래프 전체를 받는다. 별칭이 실행 의미에 남지 않도록. *)
List.concat_map let mods =
(fun (alias, p) -> Hashtbl.fold
match Hashtbl.find_opt st.modules p with (fun p (d : entry) acc ->
| Some d -> Iface.qualify alias d.iface.Iface.items { Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc)
| None -> []) st.modules []
e.imports
in in
let prog = Ir.of_module ~imports:dep_surface e.ast in let prog = Ir.of_program mods in
match Interp.run prog (main_params e.ast) with match Interp.run prog path (main_params e.ast) with
| Ok out -> Ok out | Ok out -> Ok out
| Error (pos, msg) -> Error (err_of path pos msg)) | Error (pos, msg) -> Error (err_of path pos msg))
+7 -4
View File
@@ -43,12 +43,15 @@ pub fn checkout(
} }
// 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다). // 빌린 값을 다른 빌림 자리로 넘기는 것은 허용된다 (복제가 아니다).
//
// each가 아니라 map인 이유: 결과를 버릴 방법이 언어에 없다. each는 값을
// 남기지 않는 클로저만 받으므로 Result를 삼킬 수 없고, ?는 클로저 밖으로
// 나가지 못한다. 실패를 못 본 척하려면 명시적으로 match해야 한다.
pub fn refund_all( pub fn refund_all(
pay: PaymentGateway, pay: PaymentGateway,
ids: List[OrderId], ids: List[OrderId],
) effects {PaymentGateway.refund} -> Result[Unit, PayError] { ) effects {PaymentGateway.refund} -> List[Result[Receipt, PayError]] {
List.each(ids, fn(id) { List.map(ids, fn(id) {
refund_order(pay, id)? refund_order(pay, id)
Ok(unit)
}) })
} }
+33
View File
@@ -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)
}
+4
View File
@@ -31,6 +31,10 @@
capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가 capability를 직접 정의해야 메서드의 effect가 알려지고, 05는 affinity의 뿌리가
capability라 자원 타입을 정의해야 affine임이 유도된다. capability라 자원 타입을 정의해야 affine임이 유도된다.
12는 표준 라이브러리가 생긴 뒤에야 가능해진 파일이다. std가 없을 때는
`List.each`가 모르는 이름이라 조용히 통과했다 — "모르는 것을 틀렸다고 말하지
않는다"는 맞는 원칙이지만 그 그늘에 검사되지 않는 영역이 있었다.
01~04, 06, 07은 `coolc check`를 통과한다 (exit 0). 01~04, 06, 07은 `coolc check`를 통과한다 (exit 0).
`modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라 `modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라
+2
View File
@@ -6,6 +6,8 @@
// err // err
// in scope // in scope
import "cool.dev/std/int" as Int
pub capability Console { pub capability Console {
fn print(s: String) effects {Console.print} fn print(s: String) effects {Console.print}
} }
+12
View File
@@ -3,6 +3,18 @@
// main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면 // main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면
// print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의 // 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 { pub capability Console {
fn print(s: String) effects {Console.print} fn print(s: String) effects {Console.print}
+3
View File
@@ -0,0 +1,3 @@
// 표준 라이브러리: 불리언.
pub fn show(b: Bool) -> String
+4
View File
@@ -0,0 +1,4 @@
// 표준 라이브러리: 정수.
pub fn show(n: Int) -> String
pub fn abs(n: Int) -> Int
+21
View File
@@ -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]
+4
View File
@@ -0,0 +1,4 @@
// 표준 라이브러리: 문자열.
pub fn len(s: String) -> Int
pub fn concat(a: String, b: String) -> String
+2 -1
View File
@@ -2,4 +2,5 @@
(name test_coollang) (name test_coollang)
(libraries coollang) (libraries coollang)
(deps (deps
(glob_files %{workspace_root}/samples/*.cool))) (glob_files %{workspace_root}/samples/*.cool)
(glob_files %{workspace_root}/std/*.cool)))
+55 -2
View File
@@ -999,11 +999,16 @@ let run_src src =
ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir))); ignore (Sys.command (Printf.sprintf "mkdir -p %s" (Filename.quote dir)));
let f = Filename.concat dir "m.cool" in let f = Filename.concat dir "m.cool" in
write f src; 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 Session.run st f
(* prelude가 없다. 표준 라이브러리도 명시적으로 가져온다. *)
let console = 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\ \ fn print(s: String) effects {Console.print}\n\
}\n\n" }\n\n"
@@ -1101,3 +1106,51 @@ let () =
with with
| Error e -> has_sub e.message "제공하지 않습니다" | Error e -> has_sub e.message "제공하지 않습니다"
| Ok _ -> false) | 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\
}")
= [])