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:
@@ -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
@@ -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 =
|
||||||
|
|||||||
@@ -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, [])
|
||||||
|
| None ->
|
||||||
lbind c n;
|
lbind c n;
|
||||||
IP_bind 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
|
||||||
|
(fun mi ->
|
||||||
|
let key n = mi.m_path ^ "#" ^ n in
|
||||||
List.iter
|
List.iter
|
||||||
(fun (it : Ast.item) ->
|
(fun (it : Ast.item) ->
|
||||||
match it with
|
match it with
|
||||||
| Ast.I_enum { name; variants; _ } ->
|
| Ast.I_enum { name; variants; _ } ->
|
||||||
List.iter
|
List.iter
|
||||||
(fun (v : Ast.variant) ->
|
(fun (v : Ast.variant) ->
|
||||||
Hashtbl.replace prog.ctors v.v_name (name, List.length v.v_args))
|
Hashtbl.replace prog.ctors (key v.v_name)
|
||||||
|
(name, List.length v.v_args))
|
||||||
variants
|
variants
|
||||||
| Ast.I_capability { name; methods; _ } ->
|
| Ast.I_capability { name; methods; _ } ->
|
||||||
Hashtbl.replace prog.caps name
|
Hashtbl.replace prog.caps name
|
||||||
(List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods)
|
(List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods)
|
||||||
| Ast.I_fn { decl; _ } when decl.fn_body <> None ->
|
| Ast.I_const { name; _ } ->
|
||||||
Hashtbl.replace prog.fns decl.fn_name
|
Hashtbl.replace prog.consts (key name) I_unit
|
||||||
{ fn_name = decl.fn_name; fn_params = []; fn_body = I_unit }
|
|
||||||
| Ast.I_const { name; _ } -> Hashtbl.replace prog.consts name I_unit
|
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
items;
|
mi.m_ast.items)
|
||||||
(* builtin 생성자도 IR이 알아야 한다 *)
|
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,7 +347,10 @@ 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
|
||||||
|
(fun mi ->
|
||||||
|
let c = { prog; mods = index; cur = mi; locals = [] } in
|
||||||
|
let key n = mi.m_path ^ "#" ^ n in
|
||||||
List.iter
|
List.iter
|
||||||
(fun (it : Ast.item) ->
|
(fun (it : Ast.item) ->
|
||||||
match it with
|
match it with
|
||||||
@@ -286,19 +360,24 @@ let of_module ?(imports : Ast.item list = []) (m : Ast.modul) : program =
|
|||||||
| Some body ->
|
| Some body ->
|
||||||
c.locals <- [];
|
c.locals <- [];
|
||||||
lpush c;
|
lpush c;
|
||||||
List.iter (fun (p : Ast.param) -> lbind c p.p_name) decl.fn_params;
|
List.iter
|
||||||
|
(fun (p : Ast.param) -> lbind c p.p_name)
|
||||||
|
decl.fn_params;
|
||||||
let b = lower_block c body in
|
let b = lower_block c body in
|
||||||
lpop c;
|
lpop c;
|
||||||
Hashtbl.replace prog.fns decl.fn_name
|
Hashtbl.replace prog.fns (key decl.fn_name)
|
||||||
{
|
{
|
||||||
fn_name = decl.fn_name;
|
fn_name = key decl.fn_name;
|
||||||
fn_params =
|
fn_params =
|
||||||
List.map (fun (p : Ast.param) -> p.p_name) decl.fn_params;
|
List.map
|
||||||
|
(fun (p : Ast.param) -> p.p_name)
|
||||||
|
decl.fn_params;
|
||||||
fn_body = b;
|
fn_body = b;
|
||||||
})
|
})
|
||||||
| Ast.I_const { name; value; _ } ->
|
| Ast.I_const { name; value; _ } ->
|
||||||
c.locals <- [];
|
c.locals <- [];
|
||||||
Hashtbl.replace prog.consts name (lower c value)
|
Hashtbl.replace prog.consts (key name) (lower c value)
|
||||||
| _ -> ())
|
| _ -> ())
|
||||||
items;
|
mi.m_ast.items)
|
||||||
|
mods;
|
||||||
prog
|
prog
|
||||||
|
|||||||
+61
-15
@@ -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))
|
||||||
|
|||||||
@@ -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)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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를 따라
|
||||||
|
|||||||
@@ -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}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
// 표준 라이브러리: 불리언.
|
||||||
|
|
||||||
|
pub fn show(b: Bool) -> String
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 표준 라이브러리: 정수.
|
||||||
|
|
||||||
|
pub fn show(n: Int) -> String
|
||||||
|
pub fn abs(n: Int) -> Int
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
// 표준 라이브러리: 문자열.
|
||||||
|
|
||||||
|
pub fn len(s: String) -> Int
|
||||||
|
pub fn concat(a: String, b: String) -> String
|
||||||
@@ -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
@@ -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\
|
||||||
|
}")
|
||||||
|
= [])
|
||||||
|
|||||||
Reference in New Issue
Block a user