effects: effect/capability 검사

문서가 "사활"이라고 지목한 단계다. effects 절이 여기서부터 장식이 아니라
검사 대상이 된다.

핵심 규칙 — 미선언 effect = compile error(철학 1). 수행한 자리를 들고
다니므로 진단이 함수 머리가 아니라 실제로 수행한 줄에 붙는다.

effect가 흐르는 경로 넷을 모두 막았다:
- capability 메서드 호출이 그 메서드의 선언된 effect를 요구한다
- 함수 호출이 그 함수의 effect를 물려준다
- 클로저의 effect는 정의한 자리가 아니라 부르는 자리에서 일어난다.
  클로저를 만들기만 하는 것은 effect가 아니고, 인자로 넘겨 호출되는 순간
  호출자의 것이 된다
- 파라미터가 허용한 범위를 넘는 함수를 넘기면 거부한다

effect 변수는 결정 위치에서 인자의 effect로 묶인다. 순서가 중요해서 한 번
틀렸다 — 포함 검사를 unify보다 먼저 하면 아직 해소되지 않은 미지수를 제약으로
오해해 정당한 코드를 거부한다. unify가 먼저고, 남는 차이만이 위반이다.
결정되지 않은 미지수는 판정을 미룬다 — 모르는 것을 위반이라고 말하지 않는다.

보안 정리 (i)을 직접 구현했다: capability 메서드는 값을 통해서만 부를 수
있다. 타입 이름으로 부를 수 있으면 capability 없이 effect를 수행하게 되어
정리가 무너진다.

effect 검사는 타입 검사와 같은 순회에서 돈다. effect 변수의 해소가 타입
변수와 같은 지점에서 일어나므로 떼어내면 순회와 인스턴스화를 두 번 한다.
소유는 나뉘되 순회는 하나다 — 문서에 근거를 적었다.

10_effect_errors.cool 추가. capability를 직접 정의해야 메서드의 effect가
알려지므로 외부 타입을 하나도 쓰지 않는다. 통과해야 하는 6개와 거부해야
하는 7개가 모두 의도대로 갈린다.

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 02:50:33 +09:00
co-authored by Claude Opus 5
parent 5e034c0891
commit 79e4ed4190
7 changed files with 426 additions and 48 deletions
+6
View File
@@ -268,6 +268,12 @@ Generics (철학 2에서 파생):
※ 각 단계는 그 단계가 소유한 성질만 판정한다. 예: affinity는 타입 동등성이 ※ 각 단계는 그 단계가 소유한 성질만 판정한다. 예: affinity는 타입 동등성이
아니라 substructural 성질이므로 타입 검사가 아니라 move 검사가 소유한다. 아니라 substructural 성질이므로 타입 검사가 아니라 move 검사가 소유한다.
단계가 서로의 결론을 앞지르면 진단이 엉뚱한 곳에서 난다 단계가 서로의 결론을 앞지르면 진단이 엉뚱한 곳에서 난다
※ 다만 effect 검사는 타입 검사와 같은 순회에서 돈다. effect 변수의 해소가
타입 변수와 같은 지점(호출 지점의 지역 unification)에서 일어나므로,
떼어내면 순회와 인스턴스화를 두 번 하게 된다. 소유는 나뉘되 순회는 하나다
※ v0는 과잉 선언(선언했으나 수행하지 않는 effect)을 오류로 보지 않는다.
외부 모듈의 effect를 모르는 상태에서는 판정할 수 없기 때문이다.
모듈 로딩이 생기면 lint 대상이다
- move/affinity 검사, capability use 규칙, affinity 전이 - move/affinity 검사, capability use 규칙, affinity 전이
- effect 변수 (effect 다형성) - effect 변수 (effect 다형성)
- interface artifact + hash 기반 incremental invalidation - interface artifact + hash 기반 incremental invalidation
+2 -2
View File
@@ -96,7 +96,7 @@ let check (files : string list) : (unit, error list) result =
in in
if errors <> [] then Error errors if errors <> [] then Error errors
else else
(* 타입 검사는 통과했다. 통과했다고 말하지 않는다 — 파이프라인의 (* effect 검사까지는 통과했다. 통과했다고 말하지 않는다 — 파이프라인의
나머지가 아직 없으므로 검사되지 않은 것이다. *) 나머지가 아직 없으므로 검사되지 않은 것이다. *)
Error Error
(List.map (List.map
@@ -106,7 +106,7 @@ let check (files : string list) : (unit, error list) result =
line = 0; line = 0;
col = 0; col = 0;
message = message =
"타입 검사까지 통과. effect/capability 검사와 move 검사가 아직 구현되지 않았습니다"; "effect/capability 검사까지 통과. move/affinity 검사가 아직 구현되지 않았습니다";
}) })
files) files)
+127 -23
View File
@@ -12,8 +12,10 @@ module T = Types
type error = { pos : Token.pos; msg : string } type error = { pos : Token.pos; msg : string }
type scheme = { type scheme = {
s_gen : string list; (* 타입 파라미터 이름 (effect 파라미터는 제외) *) s_gen : string list; (* 타입 파라미터 *)
s_eff_gen : string list; (* effect 파라미터 *)
s_params : T.t list; s_params : T.t list;
s_eff : T.eff; (* 이 함수를 부르면 수행되는 effect *)
s_ret : T.t; s_ret : T.t;
} }
@@ -26,6 +28,10 @@ type env = {
ctors : (string, string) Hashtbl.t; (* variant -> enum *) ctors : (string, string) Hashtbl.t; (* variant -> enum *)
mutable locals : (string * T.t) list list; mutable locals : (string * T.t) list list;
mutable ret : T.t; (* 현재 함수의 선언된 반환 타입 *) mutable ret : T.t; (* 현재 함수의 선언된 반환 타입 *)
(* 현재 본문이 수행한 effect. 위치를 같이 들고 다녀야 "어디서 수행했는지"를
말할 수 있다. 클로저에 들어가면 저장하고 비운다 — 클로저의 effect는
정의한 자리가 아니라 부르는 자리에서 일어난다. *)
mutable performed : (T.atom * Token.pos) list;
mutable errors : error list; mutable errors : error list;
} }
@@ -55,6 +61,14 @@ let lookup env n =
(* Ast.ty -> Types.t *) (* Ast.ty -> Types.t *)
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
let conv_eff_atom (a : Ast.eff_atom) : T.eff =
match a with
| Eff_var v -> [ T.A_var v ]
| Eff_set names -> List.map (fun { cap; meth } -> T.A_name (cap, meth)) names
let conv_eff_result (atoms : Ast.eff_result) : T.eff =
T.eff_resolve (List.concat_map conv_eff_atom atoms)
let rec conv env (gen : string list) (t : Ast.ty) : T.t = let rec conv env (gen : string list) (t : Ast.ty) : T.t =
match t with match t with
| T_named { name; args; _ } -> ( | T_named { name; args; _ } -> (
@@ -77,11 +91,12 @@ let rec conv env (gen : string list) (t : Ast.ty) : T.t =
|| Hashtbl.mem env.enums name || Hashtbl.mem env.caps name || Hashtbl.mem env.enums name || Hashtbl.mem env.caps name
then T.TCon (name, args) then T.TCon (name, args)
else T.TUnknown) else T.TUnknown)
| T_fn { affine; params; ret; _ } -> | T_fn { affine; params; eff; ret; _ } ->
T.TFn T.TFn
{ {
affine; affine;
params = List.map (conv env gen) params; params = List.map (conv env gen) params;
eff = (match eff with None -> [] | Some a -> conv_eff_atom a);
ret = (match ret with None -> T.TUnit | Some r -> conv env gen r); ret = (match ret with None -> T.TUnit | Some r -> conv env gen r);
} }
@@ -91,20 +106,34 @@ let scheme_of env (d : fn_decl) : scheme =
(fun g -> if g.gp_effect then None else Some g.gp_name) (fun g -> if g.gp_effect then None else Some g.gp_name)
d.fn_gen d.fn_gen
in in
let egen =
List.filter_map
(fun g -> if g.gp_effect then Some g.gp_name else None)
d.fn_gen
in
{ {
s_gen = gen; s_gen = gen;
s_eff_gen = egen;
s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params; s_params = List.map (fun p -> conv env gen p.p_ty) d.fn_params;
s_eff =
(match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms);
s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r); s_ret = (match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r);
} }
(* 호출 지점 인스턴스화: 타입 파라미터마다 새 미지수 *) (* 호출 지점 인스턴스화: 타입 파라미터마다 새 미지수 *)
let instantiate (s : scheme) = let instantiate (s : scheme) =
let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in let sub = List.map (fun v -> (v, T.fresh ())) s.s_gen in
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret) let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params,
T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret )
let instantiate_with (s : scheme) (args : T.t list) = let instantiate_with (s : scheme) (args : T.t list) =
let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in let sub = List.map2 (fun v a -> (v, a)) s.s_gen args in
(List.map (T.subst sub) s.s_params, T.subst sub s.s_ret) let esub = List.map (fun v -> (v, T.fresh_eff ())) s.s_eff_gen in
( List.map (T.subst sub esub) s.s_params,
T.subst_eff esub s.s_eff,
T.subst sub esub s.s_ret )
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
(* 내장 생성자 *) (* 내장 생성자 *)
@@ -144,8 +173,8 @@ let rec infer env (e : expr) : T.t =
| _ -> ( | _ -> (
match Hashtbl.find_opt env.fns n with match Hashtbl.find_opt env.fns n with
| Some s -> | Some s ->
let params, ret = instantiate s in let params, eff, ret = instantiate s in
T.TFn { affine = false; params; ret } T.TFn { affine = false; params; eff; ret }
| None -> ( | None -> (
match Hashtbl.find_opt env.ctors n with match Hashtbl.find_opt env.ctors n with
| Some enum -> nullary_ctor env enum n | Some enum -> nullary_ctor env enum n
@@ -268,7 +297,7 @@ and infer_struct env name fields pos =
match List.assoc_opt fname decl_fields with match List.assoc_opt fname decl_fields with
| None -> err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" name fname) | None -> err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" name fname)
| Some ft -> | Some ft ->
let want = T.subst sub ft in let want = T.subst sub [] ft in
let got = infer env fe in let got = infer env fe in
if not (T.unify want got) then if not (T.unify want got) then
mismatch env pos want got (Printf.sprintf "%s.%s 필드" name fname)) mismatch env pos want got (Printf.sprintf "%s.%s 필드" name fname))
@@ -302,19 +331,41 @@ and infer_closure env (c : closure) (expected : T.t option) =
c.cl_params expected_params c.cl_params expected_params
in in
let declared_ret = Option.map (conv env []) c.cl_ret in let declared_ret = Option.map (conv env []) c.cl_ret in
let saved = env.ret in let saved_ret = env.ret in
env.ret <- env.ret <-
(match declared_ret with (match declared_ret with
| Some t -> t | Some t -> t
| None -> ( match expected_ret with Some t -> t | None -> T.TUnknown)); | None -> ( match expected_ret with Some t -> t | None -> T.TUnknown));
(* 클로저 본문의 effect는 바깥 함수가 수행하는 것이 아니다. 저장하고 비운다. *)
let saved_perf = env.performed in
env.performed <- [];
let body = infer_block env c.cl_body in let body = infer_block env c.cl_body in
let inner = env.performed in
env.performed <- saved_perf;
let declared_eff = Option.map conv_eff_atom c.cl_eff in
let eff =
match declared_eff with
| Some d ->
(* 클로저가 effects를 명시했으면 본문이 그 안에 들어야 한다 *)
List.iter
(fun (a, pos) ->
List.iter
(fun m ->
err env pos
(Printf.sprintf "클로저가 선언하지 않은 effect %s을(를) 수행합니다 (선언: %s)"
(T.atom_show m) (T.eff_show d)))
(T.eff_missing ~declared:d ~performed:[ a ]))
inner;
d
| None -> T.eff_resolve (List.map fst inner)
in
(match declared_ret with (match declared_ret with
| Some t when not (T.unify t body) -> mismatch env c.cl_pos t body "클로저의 반환" | Some t when not (T.unify t body) -> mismatch env c.cl_pos t body "클로저의 반환"
| _ -> ()); | _ -> ());
let ret = match declared_ret with Some t -> t | None -> body in let ret = match declared_ret with Some t -> t | None -> body in
env.ret <- saved; env.ret <- saved_ret;
pop env; pop env;
T.TFn { affine = false; params = param_tys; ret } T.TFn { affine = false; params = param_tys; eff; ret }
and infer_call env callee args pos = and infer_call env callee args pos =
let fn_ty = let fn_ty =
@@ -324,12 +375,13 @@ and infer_call env callee args pos =
| Some enum -> Some (ctor_fn env enum n) | Some enum -> Some (ctor_fn env enum n)
| None -> ( | None -> (
match builtin_ctor n with match builtin_ctor n with
| Some (params, ret) -> Some (T.TFn { affine = false; params; ret }) | Some (params, ret) ->
Some (T.TFn { affine = false; params; eff = []; ret })
| None -> ( | None -> (
match Hashtbl.find_opt env.fns n with match Hashtbl.find_opt env.fns n with
| Some s -> | Some s ->
let params, ret = instantiate s in let params, eff, ret = instantiate s in
Some (T.TFn { affine = false; params; ret }) Some (T.TFn { affine = false; params; eff; ret })
| None -> None))) | None -> None)))
| _ -> ( | _ -> (
match T.resolve (infer env callee) with match T.resolve (infer env callee) with
@@ -340,7 +392,10 @@ and infer_call env callee args pos =
| None -> | None ->
List.iter (fun a -> ignore (infer env a)) args; List.iter (fun a -> ignore (infer env a)) args;
T.TUnknown T.TUnknown
| Some (T.TFn { params; ret; _ }) -> | Some (T.TFn { params; eff; ret; _ }) ->
List.iter
(fun a -> env.performed <- (a, pos) :: env.performed)
(T.eff_resolve eff);
if List.length params <> List.length args then ( if List.length params <> List.length args then (
err env pos err env pos
(Printf.sprintf "인자 %d개가 필요한데 %d개가 주어졌습니다" (List.length params) (Printf.sprintf "인자 %d개가 필요한데 %d개가 주어졌습니다" (List.length params)
@@ -354,7 +409,22 @@ and infer_call env callee args pos =
| E_closure c -> infer_closure env c (Some p) | E_closure c -> infer_closure env c (Some p)
| _ -> infer env a | _ -> infer env a
in in
if not (T.unify p got) then mismatch env pos p got "인자") (* 먼저 unify한다. 결정 위치의 effect 변수는 여기서 인자의
effect로 묶인다 — 제약이 아니라 해소다. 그 뒤에 남는 차이만이
진짜 위반이다. *)
if not (T.unify p got) then mismatch env pos p got "인자";
match (T.resolve p, T.resolve got) with
| T.TFn pf, T.TFn gf ->
let missing =
T.eff_missing ~declared:pf.eff ~performed:gf.eff
in
if missing <> [] then
err env pos
(Printf.sprintf
"넘긴 함수가 %s을(를) 수행하는데 파라미터가 허용한 effect는 %s입니다"
(String.concat ", " (List.map T.atom_show missing))
(T.eff_show pf.eff))
| _ -> ())
params args; params args;
ret ret
| Some _ -> T.TUnknown | Some _ -> T.TUnknown
@@ -366,12 +436,26 @@ and ctor_fn env enum name =
let sub = List.map (fun v -> (v, T.fresh ())) gen in let sub = List.map (fun v -> (v, T.fresh ())) gen in
let params = let params =
match List.assoc_opt name variants with match List.assoc_opt name variants with
| Some ts -> List.map (T.subst sub) ts | Some ts -> List.map (T.subst sub []) ts
| None -> [] | None -> []
in in
T.TFn { affine = false; params; ret = T.TCon (enum, List.map snd sub) } T.TFn
{
affine = false;
params;
eff = [];
ret = T.TCon (enum, List.map snd sub);
}
and infer_field env obj name pos = and infer_field env obj name pos =
(* capability 메서드는 값을 통해서만 부를 수 있다. 타입 이름으로 부를 수 있으면
capability 없이 effect를 수행하게 되어 보안 정리 (i)이 무너진다. *)
(match obj with
| E_ident (n, _) when lookup env n = None && Hashtbl.mem env.caps n ->
err env pos
(Printf.sprintf
"capability %s의 메서드는 값을 통해서만 부를 수 있습니다 (%s를 파라미터로 받아야 합니다)" n n)
| _ -> ());
let t = infer env obj in let t = infer env obj in
match T.resolve t with match T.resolve t with
| T.TUnknown -> T.TUnknown | T.TUnknown -> T.TUnknown
@@ -380,7 +464,7 @@ and infer_field env obj name pos =
| Some (gen, fields) -> ( | Some (gen, fields) -> (
let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in let sub = List.map2 (fun v a -> (v, a)) gen (adjust gen args) in
match List.assoc_opt name fields with match List.assoc_opt name fields with
| Some ft -> T.subst sub ft | Some ft -> T.subst sub [] ft
| None -> | None ->
err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name); err env pos (Printf.sprintf "%s에 %s 필드가 없습니다" cname name);
T.TUnknown) T.TUnknown)
@@ -389,8 +473,8 @@ and infer_field env obj name pos =
| Some methods -> ( | Some methods -> (
match List.assoc_opt name methods with match List.assoc_opt name methods with
| Some s -> | Some s ->
let params, ret = instantiate s in let params, eff, ret = instantiate s in
T.TFn { affine = false; params; ret } T.TFn { affine = false; params; eff; ret }
| None -> | None ->
err env pos err env pos
(Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name); (Printf.sprintf "capability %s에 %s 메서드가 없습니다" cname name);
@@ -428,8 +512,8 @@ and infer_inst env callee args pos =
(List.length s.s_gen) (List.length tys)); (List.length s.s_gen) (List.length tys));
T.TUnknown) T.TUnknown)
else else
let params, ret = instantiate_with s tys in let params, eff, ret = instantiate_with s tys in
T.TFn { affine = false; params; ret }) T.TFn { affine = false; params; eff; ret })
| _ -> T.TUnknown | _ -> T.TUnknown
and check_pattern env (scrutinee : T.t) (p : pattern) = and check_pattern env (scrutinee : T.t) (p : pattern) =
@@ -466,7 +550,7 @@ and check_ctor env scrutinee enum name args pos =
in in
if List.length fields = List.length args then if List.length fields = List.length args then
List.iter2 List.iter2
(fun ft ap -> check_pattern env (T.subst sub ft) ap) (fun ft ap -> check_pattern env (T.subst sub [] ft) ap)
fields args fields args
(* ------------------------------------------------------------------ *) (* ------------------------------------------------------------------ *)
@@ -527,10 +611,29 @@ let check_fn env (d : fn_decl) =
match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r match d.fn_ret with None -> T.TUnit | Some r -> conv env gen r
in in
env.ret <- declared; env.ret <- declared;
env.performed <- [];
let got = infer_block env body in let got = infer_block env body in
if not (T.unify declared got) then if not (T.unify declared got) then
mismatch env d.fn_pos declared got mismatch env d.fn_pos declared got
(Printf.sprintf "%s의 본문이 남기는 값" d.fn_name); (Printf.sprintf "%s의 본문이 남기는 값" d.fn_name);
(* 미선언 effect = compile error (철학 1).
수행한 자리를 알고 있으므로 그 자리에 진단을 붙인다. *)
let declared_eff =
match d.fn_eff with None -> [] | Some atoms -> conv_eff_result atoms
in
List.iter
(fun (a, pos) ->
match T.eff_missing ~declared:declared_eff ~performed:[ a ] with
| [] -> ()
| missing ->
List.iter
(fun m ->
err env pos
(Printf.sprintf "선언되지 않은 effect %s (%s의 effects 절은 %s입니다)"
(T.atom_show m) d.fn_name (T.eff_show declared_eff)))
missing)
(List.rev env.performed);
env.performed <- [];
pop env pop env
let check (m : modul) : error list = let check (m : modul) : error list =
@@ -544,6 +647,7 @@ let check (m : modul) : error list =
ctors = Hashtbl.create 16; ctors = Hashtbl.create 16;
locals = []; locals = [];
ret = T.TUnit; ret = T.TUnit;
performed = [];
errors = []; errors = [];
} }
in in
+103 -15
View File
@@ -1,11 +1,12 @@
(* 타입 표현과 지역 unification. (* 타입과 effect 표현, 그리고 지역 unification.
TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서 TUnknown이 핵심이다. 외부 모듈에서 오는 이름은 모듈 로딩이 없는 v0에서
해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을 해소할 수 없다. 그런 타입은 TUnknown이 되고 무엇과도 맞는다 — 모르는 것을
틀렸다고 말하지 않기 위해서다. 아는 범위에서만 검사한다. 틀렸다고 말하지 않기 위해서다.
TMeta는 호출 지점에서 제네릭을 인스턴스화할 때 생기는 미지수다. 함수 하나 effect는 순서 없는 집합이고 합성은 합집합이다. 변수는 집합 변수이며,
범위에서만 살고 전역으로 흐르지 않는다 (철학 2: 전역 추론 없음). *) 호출 지점에서 결정 위치(파라미터의 effect 자리에 단독으로 선 변수)를 통해
메타에 묶인다. 함수 하나 범위를 넘지 않는다. *)
type t = type t =
| TUnknown | TUnknown
@@ -15,20 +16,84 @@ type t =
| TUnit | TUnit
| TVar of string | TVar of string
| TCon of string * t list | TCon of string * t list
| TFn of { affine : bool; params : t list; ret : t } | TFn of { affine : bool; params : t list; eff : eff; ret : t }
| TMeta of meta ref | TMeta of meta ref
and meta = Unbound of int | Bound of t and meta = Unbound of int | Bound of t
(* effect 집합. 원소는 구체 이름, 집합 변수, 또는 호출 지점의 미지수다. *)
and eff = atom list
and atom =
| A_name of string * string (* Cap.method *)
| A_var of string
| A_meta of emeta ref
and emeta = EUnbound of int | EBound of eff
let counter = ref 0 let counter = ref 0
let fresh () = let fresh () =
incr counter; incr counter;
TMeta (ref (Unbound !counter)) TMeta (ref (Unbound !counter))
let fresh_eff () =
incr counter;
[ A_meta (ref (EUnbound !counter)) ]
let rec resolve t = let rec resolve t =
match t with TMeta { contents = Bound u } -> resolve u | _ -> t match t with TMeta { contents = Bound u } -> resolve u | _ -> t
(* 묶인 메타를 펼치고 중복을 없앤다. 집합이므로 순서는 의미가 없다. *)
let rec eff_resolve (e : eff) : eff =
let expand a =
match a with
| A_meta { contents = EBound inner } -> eff_resolve inner
| _ -> [ a ]
in
let flat = List.concat_map expand e in
let mem a acc =
List.exists
(fun b ->
match (a, b) with
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
| A_var x, A_var y -> x = y
| A_meta r, A_meta r' -> r == r'
| _ -> false)
acc
in
List.fold_left (fun acc a -> if mem a acc then acc else a :: acc) [] flat
|> List.rev
let atom_show = function
| A_name (c, m) -> c ^ "." ^ m
| A_var v -> v
| A_meta { contents = EUnbound n } -> Printf.sprintf "_e%d" n
| A_meta { contents = EBound _ } -> "?"
let eff_show e =
match eff_resolve e with
| [] -> "{}"
| atoms -> "{" ^ String.concat ", " (List.map atom_show atoms) ^ "}"
let atom_eq a b =
match (a, b) with
| A_name (c1, m1), A_name (c2, m2) -> c1 = c2 && m1 = m2
| A_var x, A_var y -> x = y
| A_meta r, A_meta r' -> r == r'
| _ -> false
(* declared가 덮지 못하는 원소들. 미지수는 판정을 미룬다 —
결정되지 않은 것을 위반이라고 말하지 않는다. *)
let eff_missing ~declared ~performed =
let declared = eff_resolve declared in
List.filter
(fun a ->
match a with
| A_meta { contents = EUnbound _ } -> false
| _ -> not (List.exists (atom_eq a) declared))
(eff_resolve performed)
let rec show t = let rec show t =
match resolve t with match resolve t with
| TUnknown -> "?" | TUnknown -> "?"
@@ -39,10 +104,11 @@ let rec show t =
| TVar v -> v | TVar v -> v
| TCon (n, []) -> n | TCon (n, []) -> n
| TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]" | TCon (n, args) -> n ^ "[" ^ String.concat ", " (List.map show args) ^ "]"
| TFn { affine; params; ret } -> ( | TFn { affine; params; eff; ret } -> (
(if affine then "affine fn(" else "fn(") (if affine then "affine fn(" else "fn(")
^ String.concat ", " (List.map show params) ^ String.concat ", " (List.map show params)
^ ")" ^ ")"
^ (match eff_resolve eff with [] -> "" | e -> " effects " ^ eff_show e)
^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r) ^ match resolve ret with TUnit -> "" | r -> " -> " ^ show r)
| TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n | TMeta { contents = Unbound n } -> Printf.sprintf "_%d" n
| TMeta { contents = Bound _ } -> "?" | TMeta { contents = Bound _ } -> "?"
@@ -54,8 +120,19 @@ let rec occurs r t =
| TFn { params; ret; _ } -> List.exists (occurs r) params || occurs r ret | TFn { params; ret; _ } -> List.exists (occurs r) params || occurs r ret
| _ -> false | _ -> false
(* 성공하면 true. 실패해도 예외를 던지지 않는다 — 호출자가 위치를 알고 (* 결정 위치의 해소: 파라미터의 effect 자리에 단독으로 선 미지수만 묶는다.
진단을 만든다. *) 그 외에는 참을 돌려주고, 실제 포함 검사는 호출 지점에서 방향을 아는
쪽이 한다 (진단 품질 때문에). *)
let unify_eff a b =
match (eff_resolve a, eff_resolve b) with
| [ A_meta ({ contents = EUnbound _ } as r) ], other ->
r := EBound other;
true
| other, [ A_meta ({ contents = EUnbound _ } as r) ] ->
r := EBound other;
true
| _ -> true
let rec unify a b = let rec unify a b =
match (resolve a, resolve b) with match (resolve a, resolve b) with
| TUnknown, _ | _, TUnknown -> true | TUnknown, _ | _, TUnknown -> true
@@ -75,19 +152,30 @@ let rec unify a b =
소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *) 소유한다. 여기서 섞으면 두 검사가 서로의 결론을 앞질러 버린다. *)
List.length f.params = List.length g.params List.length f.params = List.length g.params
&& List.for_all2 unify f.params g.params && List.for_all2 unify f.params g.params
&& unify f.ret g.ret && unify_eff f.eff g.eff && unify f.ret g.ret
| _ -> false | _ -> false
(* 제네릭 인스턴스화: TVar를 주어진 대입으로 바꾼다 *) (* 제네릭 인스턴스화: 타입 변수와 effect 변수를 동시에 바꾼다 *)
let rec subst env t = let rec subst tenv eenv t =
match resolve t with match resolve t with
| TVar v -> ( match List.assoc_opt v env with Some u -> u | None -> TVar v) | TVar v -> ( match List.assoc_opt v tenv with Some u -> u | None -> TVar v)
| TCon (n, args) -> TCon (n, List.map (subst env) args) | TCon (n, args) -> TCon (n, List.map (subst tenv eenv) args)
| TFn f -> | TFn f ->
TFn TFn
{ {
affine = f.affine; affine = f.affine;
params = List.map (subst env) f.params; params = List.map (subst tenv eenv) f.params;
ret = subst env f.ret; eff = subst_eff eenv f.eff;
ret = subst tenv eenv f.ret;
} }
| u -> u | u -> u
and subst_eff eenv (e : eff) : eff =
eff_resolve
(List.concat_map
(fun a ->
match a with
| A_var v -> (
match List.assoc_opt v eenv with Some s -> s | None -> [ a ])
| _ -> [ a ])
e)
+86
View File
@@ -0,0 +1,86 @@
// 10. effect 검사기가 거부해야 하는 코드
//
// 09와 같은 이유로 외부 타입이 하나도 없다. capability를 이 파일에서 정의해야
// 메서드의 effect가 알려지고, 검사기가 실제로 판정할 수 있다.
pub capability Db {
fn read(id: Int) effects {Db.read} -> Int
fn write(id: Int, v: Int) effects {Db.write}
}
pub capability Log {
fn write(msg: String) effects {Log.write}
}
// --- 통과해야 하는 것 ---
pub fn get(db: Db, id: Int) effects {Db.read} -> Int {
db.read(id)
}
pub fn copy(db: Db, from: Int, to: Int) effects {Db.read, Db.write} {
db.write(to, db.read(from))
}
// 헬퍼를 부르면 헬퍼의 effect를 물려받는다
pub fn get_twice(db: Db, id: Int) effects {Db.read} -> Int {
get(db, id) + get(db, id)
}
// effect 변수: 결정 위치의 변수가 인자의 effect로 묶인다
pub fn twice[e: effects](f: fn() effects e) effects e {
f()
f()
}
pub fn log_twice(log: Log) effects {Log.write} {
twice(fn() { log.write("hi") })
}
// effect 없는 함수는 effects 절이 없다
pub fn pure_add(a: Int, b: Int) -> Int {
a + b
}
// --- 여기서부터 전부 오류다 ---
// [E-effect-undeclared] 선언 없이 capability 메서드를 부른다
pub fn silent_read(db: Db) -> Int {
db.read(1)
}
// [E-effect-undeclared] 일부만 선언했다
pub fn partial(db: Db, id: Int) effects {Db.read} {
db.write(id, db.read(id))
}
// [E-effect-undeclared] 헬퍼가 수행하는 effect도 물려받아야 한다
pub fn via_helper(db: Db, id: Int) -> Int {
get(db, id)
}
// [E-effect-undeclared] 클로저를 통해 새어 나오는 effect
pub fn via_closure(log: Log) {
twice(fn() { log.write("hi") })
}
// [E-effect-closure-annotated] 클로저가 선언한 것보다 많이 수행한다
pub fn closure_lies(log: Log) effects {Log.write} {
twice(fn() effects {} { log.write("hi") })
}
// [E-effect-param] 파라미터가 허용한 effect를 넘는 함수를 넘긴다
pub fn takes_pure(f: fn() effects {}) {
f()
}
pub fn pass_impure(log: Log) effects {Log.write} {
takes_pure(fn() { log.write("hi") })
}
// [E-capability-static] capability 메서드를 타입 이름으로 부른다.
// 이것이 허용되면 capability 없이 effect를 수행할 수 있게 되어
// "capability 없이는 effect를 수행할 수 없다"는 정리가 무너진다.
pub fn no_instance() effects {Db.read} -> Int {
Db.read(1)
}
+8 -5
View File
@@ -17,14 +17,17 @@
| 07_module_interface | interface artifact가 담아야 할 것 전부 | | 07_module_interface | interface artifact가 담아야 할 것 전부 |
| 08_syntax_errors | **파서가** 거부해야 하는 코드 | | 08_syntax_errors | **파서가** 거부해야 하는 코드 |
| 09_type_errors | **타입 검사기가** 거부해야 하는 코드 (외부 타입 0개) | | 09_type_errors | **타입 검사기가** 거부해야 하는 코드 (외부 타입 0개) |
| 10_effect_errors | **effect 검사기가** 거부해야 하는 코드 (capability를 직접 정의) |
05, 08, 09 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가 기대 05, 08, 09, 10은 통과하면 안 되는 파일이다. 각 함수 주석의 `[E-...]` 태그가
진단이며, 의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가, 05는 아직 기대 진단이며, 의 목적이 다르다 — **08은 파서가, 09는 타입 검사기가,
없는 move/affinity 검사가** 거부해야 한다. 단계별로 파일을 나눈 이유는 10은 effect 검사기가, 05는 아직 없는 move/affinity 검사가** 거부해야 한다.
앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에 영영 도달하지 못하기 때문이다. 단계별로 파일을 나눈 이유는 앞 단계가 첫 오류에서 멈추면 뒤 단계 케이스에
영영 도달하지 못하기 때문이다.
09에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가 09와 10에는 외부 타입이 하나도 없다. 전부 모듈 안에서 정의되므로 검사기가
TUnknown으로 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다. TUnknown으로 빠져나갈 구석이 없다 — 검사기에 이빨이 있는지 보는 파일이다.
10은 capability를 직접 정의해야 메서드의 effect가 알려지므로 특히 그렇다.
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다. 파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
+94 -3
View File
@@ -580,7 +580,9 @@ let () =
Sys.readdir dir |> Array.to_list Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool") |> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.filter (fun f -> |> List.filter (fun f ->
f <> "08_syntax_errors.cool" && f <> "09_type_errors.cool") f <> "08_syntax_errors.cool"
&& f <> "09_type_errors.cool"
&& f <> "10_effect_errors.cool")
|> List.sort compare |> List.sort compare
in in
List.iter List.iter
@@ -593,7 +595,96 @@ let () =
errors; errors;
check (f ^ " 타입 검사") false) check (f ^ " 타입 검사") false)
ok_files; ok_files;
match Driver.typecheck (Filename.concat dir "09_type_errors.cool") with (match Driver.typecheck (Filename.concat dir "09_type_errors.cool") with
| Ok () -> check "09는 타입 오류를 내야 한다" false | Ok () -> check "09는 타입 오류를 내야 한다" false
| Error errors -> | Error errors ->
check "09의 오류를 전부 모은다 (첫 오류에서 멈추지 않는다)" (List.length errors >= 18) check "09의 오류를 전부 모은다 (첫 오류에서 멈추지 않는다)" (List.length errors >= 18));
match Driver.typecheck (Filename.concat dir "10_effect_errors.cool") with
| Ok () -> check "10은 effect 오류를 내야 한다" false
| Error errors -> check "10의 effect 오류" (List.length errors >= 6)
(* ================================================================== *)
(* effect / capability 검사 *)
(* ================================================================== *)
let cap =
"capability Db {\n\
\ fn read(id: Int) effects {Db.read} -> Int\n\
\ fn touch(id: Int) effects {Db.read}\n\
}\n"
(* --- 미선언 effect = compile error (철학 1) --- *)
let () =
check "선언하면 통과"
(type_ok (cap ^ "fn f(db: Db) effects {Db.read} -> Int {\n db.read(1)\n}"));
check "선언 없이 capability 메서드를 부르면 오류"
(type_has
(cap ^ "fn f(db: Db) -> Int {\n db.read(1)\n}")
"선언되지 않은 effect Db.read");
check "헬퍼의 effect도 물려받는다"
(type_has
(cap
^ "fn g(db: Db) effects {Db.read} -> Int {\n\
\ db.read(1)\n\
}\n\
fn f(db: Db) -> Int {\n\
\ g(db)\n\
}")
"선언되지 않은 effect Db.read");
check "effect 없는 함수는 절이 없어도 된다"
(type_ok "fn add(a: Int, b: Int) -> Int {\n a + b\n}")
(* --- 클로저의 effect는 정의한 자리가 아니라 부르는 자리에서 일어난다 --- *)
let () =
check "클로저를 만들기만 하면 effect가 새지 않는다"
(type_ok
(cap
^ "fn f(db: Db) -> fn() effects {Db.read} -> Int {\n\
\ fn() { db.read(1) }\n\
}"));
check "클로저가 선언한 것보다 많이 수행하면 오류"
(type_has
(cap
^ "fn run(f: fn() effects {}) \n\
fn f(db: Db) {\n\
\ run(fn() effects {} { db.touch(1) })\n\
}")
"클로저가 선언하지 않은 effect Db.read");
check "파라미터가 허용한 범위를 넘는 함수를 넘기면 오류"
(type_has
(cap
^ "fn run(f: fn() effects {})\n\
fn f(db: Db) {\n\
\ run(fn() { db.touch(1) })\n\
}")
"파라미터가 허용한 effect는 {}")
(* --- effect 변수: 결정 위치에서 인자의 effect로 묶인다 --- *)
let () =
let twice = "fn twice[e: effects](f: fn() effects e) effects e\n" in
check "effect 변수는 인자의 effect로 해소된다"
(type_ok
(cap ^ twice
^ "fn f(db: Db) effects {Db.read} {\n twice(fn() { db.touch(1) })\n}"));
check "해소된 effect가 선언에 없으면 오류"
(type_has
(cap ^ twice ^ "fn f(db: Db) {\n twice(fn() { db.touch(1) })\n}")
"선언되지 않은 effect Db.read");
check "effect 변수를 그대로 물려주는 것은 통과"
(type_ok
(twice ^ "fn g[e: effects](f: fn() effects e) effects e {\n twice(f)\n}"));
check "effect 변수를 선언하지 않고 물려주면 오류"
(type_has
(twice ^ "fn g[e: effects](f: fn() effects e) {\n twice(f)\n}")
"선언되지 않은 effect e")
(* --- capability 없이는 effect를 수행할 수 없다 --- *)
let () =
check "capability 값이 없으면 메서드를 부를 수 없다"
(type_has
(cap ^ "fn f() effects {Db.read} -> Int {\n Db.read(1)\n}")
"값을 통해서만")