Files
coollang/lib/move.ml
T
coolguyandClaude Opus 5 d85816abae own: 함수 타입과 클로저 파라미터의 소유권 — 구멍이 숨기던 버그가 나왔다
D5를 고친다. 함수 타입에 own을 적을 수 없어 "소유권을 가져가는 클로저"를
표현할 수 없었고, move 검사기가 클로저 파라미터를 무조건 소유로 봐서 고차
경계에서 소유권 검사가 뚫려 있었다.

클로저 파라미터의 소유권은 리터럴이 스스로 적는다. 타입은 기대 타입에서
읽어오지만 소유권은 읽어오지 않는다 — move 검사는 타입 검사와 별도 순회라
타입을 모르고, 소유권은 타입보다 결과가 크기 때문이다.
unify는 정확히 일치를 요구한다. 방향을 다루려면 부분 타입이 필요하고 없다.

그리고 구멍이 자기가 숨긴 버그를 덮고 있었다. std/list.cool의 fold가
f: fn(acc, a) -> acc 로 적혀 있었는데 틀렸다 — 누적자는 매 단계 소비되고
새것으로 바뀌므로 own이다. 빌림으로 적혀 있어 affine 값을 fold로 실어나를
수 없었는데, 클로저 파라미터를 소유로 봤으니 아무 오류도 안 났다.
고치니 samples/app이 즉시 깨졌고, own을 붙여 고쳤다.

남은 한계를 기록했다: move 검사는 타입이 없어 제네릭을 통과해 affinity를
보지 못한다. 양쪽 다 표기가 없으면 통과한다. 근본 해법은 두 순회를 합치는
것이고 v0에서는 하지 않는다.

대가도 기록했다: own이 흔해진다. fold가 항상 요구하므로 copyable 누적자에도
붙는다. 표기의 신호가 약해지는지 지켜본다.

문법 먼저 고치고 대조 장치가 파서를 지적하게 했다. 지금은 문장 500개,
파일 29개 모두 갈림 0건.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
2026-08-30 19:06:17 +09:00

509 lines
18 KiB
OCaml

(* move / affinity 검사.
보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로
닫는 단계다. 검사는 전부 함수 로컬 데이터플로우다. 전역 분석이 없다.
affinity의 뿌리는 capability다. capability를 필드로 가진 타입은 전이적으로
affine이고(철학 3), 이 전이가 없으면 wrapper 하나를 복사해 capability가
사실상 복제된다.
외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다 — 모르는 것을
위반이라고 말하지 않는다. 그래서 이 검사를 시험하려면 자원 타입을 모듈
안에서 정의해야 한다 (samples/05). *)
open Ast
type error = { pos : Token.pos; msg : string }
(* 값이 무엇인가: 소유한 affine 값인가, 빌린 값인가. 둘은 직교한다. *)
type vinfo = { v_affine : bool; v_use : bool }
let v_copy = { v_affine = false; v_use = false }
type binding = {
b_id : int;
b_name : string;
b_affine : bool;
b_use : bool;
b_mut : bool;
b_depth : int;
}
type fninfo = { f_params : param list; f_ret : Ast.ty option }
type state = {
aff : (string, bool) Hashtbl.t;
fns : (string, fninfo) Hashtbl.t;
meths : (string, (string * fninfo) list) Hashtbl.t;
mutable scopes : binding list list;
moved : (int, Token.pos) Hashtbl.t;
mutable next_id : int;
mutable depth : int;
(* 클로저 프레임: (프레임 깊이, 잡아온 바깥 바인딩). 중첩 클로저를 위해 스택 *)
mutable frames : (int * binding list ref) list;
(* 클로저 인자를 걸을 때 기대 파라미터 타입. move 검사는 타입 검사와 별도
순회라 타입을 모르는데, 클로저 파라미터의 affinity를 알아야 빌림 여부를
판정할 수 있다. 호출 대상의 시그니처에서 읽어와 여기 잠깐 둔다. *)
mutable cl_expect : fn_param_ty list option;
mutable errors : error list;
}
let err st pos msg = st.errors <- { pos; msg } :: st.errors
(* ------------------------------------------------------------------ *)
(* affinity 유도 *)
(* ------------------------------------------------------------------ *)
let builtin_containers = [ "List"; "Option"; "Result" ]
let rec ty_affine st (t : Ast.ty) =
match t with
| T_fn { affine; _ } -> affine
| T_named { modl; name; args; _ } ->
let name = match modl with Some a -> a ^ "." ^ name | None -> name in
let self =
match Hashtbl.find_opt st.aff name with Some b -> b | None -> false
in
let arg_affine =
List.exists
(function TA_ty t -> ty_affine st t | TA_eff _ -> false)
args
in
self || (List.mem name builtin_containers && arg_affine) || arg_affine
(* capability가 뿌리다. struct/enum은 필드에서 전이된다. 상호 재귀 타입을 위해
변화가 없을 때까지 돈다. *)
let derive_affinity st (items : item list) =
List.iter
(fun it ->
match it with
| I_capability { name; _ } -> Hashtbl.replace st.aff name true
| I_struct { name; _ } -> Hashtbl.replace st.aff name false
| I_enum { name; _ } -> Hashtbl.replace st.aff name false
| _ -> ())
items;
let changed = ref true in
while !changed do
changed := false;
List.iter
(fun it ->
let update name affine =
if affine && Hashtbl.find_opt st.aff name <> Some true then (
Hashtbl.replace st.aff name true;
changed := true)
in
match it with
| I_struct { name; fields; _ } ->
update name (List.exists (fun f -> ty_affine st f.f_ty) fields)
| I_enum { name; variants; _ } ->
update name
(List.exists
(fun v -> List.exists (ty_affine st) v.v_args)
variants)
| _ -> ())
items
done;
(* copyable 선언과 affine 필드는 공존할 수 없다 *)
List.iter
(fun it ->
match it with
| I_struct { copyable = true; name; fields; pos; _ } ->
List.iter
(fun f ->
if ty_affine st f.f_ty then
err st f.f_pos
(Printf.sprintf "%s은(는) copyable로 선언되었지만 %s 필드가 affine입니다"
name f.f_name))
fields;
ignore pos
| _ -> ())
items
(* ------------------------------------------------------------------ *)
(* 스코프 *)
(* ------------------------------------------------------------------ *)
let push st = st.scopes <- [] :: st.scopes
let pop st = match st.scopes with _ :: r -> st.scopes <- r | [] -> ()
let add st name ~affine ~use ~mut_ =
st.next_id <- st.next_id + 1;
let b =
{
b_id = st.next_id;
b_name = name;
b_affine = affine;
b_use = use;
b_mut = mut_;
b_depth = st.depth;
}
in
(match st.scopes with
| s :: r -> st.scopes <- (b :: s) :: r
| [] -> st.scopes <- [ [ b ] ]);
b
let find st name =
let rec go = function
| [] -> None
| s :: r -> (
match List.find_opt (fun b -> b.b_name = name) s with
| Some b -> Some b
| None -> go r)
in
go st.scopes
(* 클로저 안에서 바깥 바인딩을 건드리면 capture다. 프레임마다 기록한다. *)
let note_capture st b =
List.iter
(fun (fdepth, acc) ->
if b.b_depth < fdepth && not (List.exists (fun x -> x.b_id = b.b_id) !acc)
then acc := b :: !acc)
st.frames
(* ------------------------------------------------------------------ *)
(* 분기 병합 — 보수적 합집합 *)
(* ------------------------------------------------------------------ *)
let snapshot st = Hashtbl.copy st.moved
let restore st snap =
Hashtbl.reset st.moved;
Hashtbl.iter (fun k v -> Hashtbl.replace st.moved k v) snap
let merge st snaps =
(* 한 분기에서라도 moved면 병합 지점 이후 moved *)
Hashtbl.reset st.moved;
List.iter
(fun snap ->
Hashtbl.iter
(fun k v ->
if not (Hashtbl.mem st.moved k) then Hashtbl.replace st.moved k v)
snap)
snaps
(* ------------------------------------------------------------------ *)
(* 식 *)
(* ------------------------------------------------------------------ *)
type ctx = Borrow | Move of string (* 어디로 옮겨가는지 — 진단에 쓴다 *)
let rec walk st (ctx : ctx) (e : expr) : vinfo =
match e with
| E_lit _ -> v_copy
(* crash는 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
| E_crash { msg; _ } ->
ignore (walk st Borrow msg);
v_copy
| E_ident (n, pos) -> (
match find st n with
| None -> v_copy
| Some b ->
note_capture st b;
(match Hashtbl.find_opt st.moved b.b_id with
| Some mp ->
err st pos
(Printf.sprintf "%s은(는) 이미 move되었습니다 (%d:%d에서 소비)" n
mp.Token.line mp.Token.col)
| None -> ());
(match ctx with
| Borrow -> ()
| Move where ->
if b.b_use then
err st pos
(Printf.sprintf "%s은(는) 빌린 값이라 %s 없습니다 (own으로 받아야 합니다)" n
where)
else if b.b_affine then Hashtbl.replace st.moved b.b_id pos);
{ v_affine = b.b_affine; v_use = b.b_use })
| E_list (xs, _) ->
let infos = List.map (walk st (Move "컨테이너에 넣을 수")) xs in
{ v_affine = List.exists (fun i -> i.v_affine) infos; v_use = false }
| E_struct { name; fields; _ } ->
List.iter (fun (_, e) -> ignore (walk st (Move "struct에 저장할 수") e)) fields;
{
v_affine =
(match Hashtbl.find_opt st.aff name with
| Some b -> b
| None -> false);
v_use = false;
}
| E_closure c -> (
let r = walk_closure st c in
(* use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라
소유를 가져가는 자리로 갈 수 없다. 별도의 nonescaping 개념 없이
use 규칙 하나로 닫힌다. *)
match ctx with
| Move where when r.v_use ->
err st c.cl_pos
(Printf.sprintf "빌린 값을 capture한 클로저는 %s 없습니다 (use 값은 탈출하지 못합니다)"
where);
r
| _ -> r)
| E_if { cond; then_; else_; _ } ->
ignore (walk st Borrow cond);
let before = snapshot st in
let t1 = walk_block st ctx then_ in
let s1 = snapshot st in
restore st before;
let t2 = match else_ with None -> v_copy | Some e -> walk st ctx e in
let s2 = snapshot st in
merge st [ s1; s2 ];
{ v_affine = t1.v_affine || t2.v_affine; v_use = t1.v_use || t2.v_use }
| E_match { scrutinee; arms; _ } ->
let sinfo = walk st Borrow scrutinee in
let before = snapshot st in
let results =
List.map
(fun a ->
restore st before;
push st;
bind_pattern st sinfo a.arm_pat;
let r = walk st ctx a.arm_body in
pop st;
(r, snapshot st))
arms
in
if results <> [] then merge st (List.map snd results);
{
v_affine = List.exists (fun (r, _) -> r.v_affine) results;
v_use = List.exists (fun (r, _) -> r.v_use) results;
}
| E_scope { name; parent; body; _ } ->
ignore (walk st Borrow (E_ident (parent, Token.{ line = 0; col = 0 })));
push st;
(* 자식 TaskScope는 second-class다 — 블록 밖으로 나갈 수 없다 *)
ignore (add st name ~affine:true ~use:true ~mut_:false);
let r = walk_block st ctx body in
pop st;
r
| E_block b ->
push st;
let r = walk_block st ctx b in
pop st;
r
| E_call { callee; args; pos } -> walk_call st callee args pos
| E_field { obj; _ } ->
(* v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다. *)
let o = walk st Borrow obj in
if o.v_affine || o.v_use then { v_affine = true; v_use = true }
else v_copy
| E_inst { callee; _ } -> walk st Borrow callee
| E_try { inner; _ } -> walk st ctx inner
| E_unary { operand; _ } -> walk st Borrow operand
| E_binary { lhs; rhs; _ } ->
ignore (walk st Borrow lhs);
ignore (walk st Borrow rhs);
v_copy
and bind_pattern st (info : vinfo) p =
match p with
| P_wild _ | P_lit _ -> ()
| P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_:false)
| P_ctor { args; _ } -> List.iter (bind_pattern st info) args
and walk_block st ctx (b : block) : vinfo =
let rec go = function
| [] -> v_copy
| [ S_expr e ] -> walk st ctx e (* 꼬리 식은 블록의 값이다 *)
| s :: rest ->
walk_stmt st s;
go rest
in
go b.stmts
and walk_stmt st = function
| S_let { mut_; pat; value; _ } ->
let info = walk st (Move "다른 이름에 묶을 수") value in
let rec bind p =
match p with
| P_bind (n, _) ->
ignore (add st n ~affine:info.v_affine ~use:info.v_use ~mut_)
| P_ctor { args; _ } -> List.iter bind args
| _ -> ()
in
bind pat
| S_return { value; _ } -> (
match value with
| None -> ()
| Some e -> ignore (walk st (Move "반환할 수") e))
| S_assign { place; value; _ } ->
ignore (walk st (Move "대입할 수") value);
ignore (walk st Borrow place)
| S_expr e -> ignore (walk st Borrow e)
(* 클로저: 무엇을 잡아왔는지가 클로저 자신의 성질을 정한다 (전이 규칙) *)
and walk_closure st (c : closure) : vinfo =
st.depth <- st.depth + 1;
let acc = ref [] in
st.frames <- (st.depth, acc) :: st.frames;
push st;
let expect = st.cl_expect in
st.cl_expect <- None;
List.iteri
(fun i (p : cl_param) ->
let affine =
match p.cp_ty with
| Some t -> ty_affine st t
| None -> (
(* 표기가 없으면 호출 대상의 시그니처에서 읽어온다 *)
match Option.bind expect (fun ps -> List.nth_opt ps i) with
| Some pt -> ty_affine st pt.pt_ty
| None -> false)
in
(* 함수 파라미터와 같은 규칙 — 무표기는 빌림이다. 전에는 클로저
파라미터를 무조건 소유로 봤고, 그래서 고차 경계에서 소유권 검사가
뚫렸다 (dogfoods/FINDINGS D5). *)
let use_ = affine && not p.cp_own in
ignore (add st p.cp_name ~affine ~use:use_ ~mut_:false))
c.cl_params;
ignore (walk_block st (Move "반환할 수") c.cl_body);
pop st;
st.frames <- List.tl st.frames;
st.depth <- st.depth - 1;
let captured = !acc in
let affine = ref false and use_ = ref false in
List.iter
(fun b ->
if b.b_mut then
err st c.cl_pos
(Printf.sprintf
"클로저는 mut 바인딩 %s을(를) capture할 수 없습니다 (v0에 참조가 없으므로 별칭도 복사도 만들지 \
않는다)"
b.b_name);
if b.b_use then use_ := true;
if b.b_affine && not b.b_use then (
(* by-move capture: 바깥에서는 여기서 소비된다 *)
affine := true;
if not (Hashtbl.mem st.moved b.b_id) then
Hashtbl.replace st.moved b.b_id c.cl_pos))
captured;
{ v_affine = !affine; v_use = !use_ }
and walk_call st callee args pos =
let info = callee_info st callee in
ignore (walk st Borrow callee);
let params = match info with Some f -> f.f_params | None -> [] in
List.iteri
(fun i a ->
let p = List.nth_opt params i in
let own = match p with Some p -> p.p_own | None -> false in
let pty = Option.map (fun p -> p.p_ty) p in
let ctx = if own then Move "다른 함수에 넘길 수" else Borrow in
(match (a, pty) with
| E_closure _, Some (T_fn { params; _ }) -> st.cl_expect <- Some params
| _ -> st.cl_expect <- None);
let got = walk st ctx a in
(* callable affinity: affine 클로저를 fn 자리에 넘길 수 없다 *)
match pty with
| Some (T_fn { affine = false; _ }) when got.v_affine && not got.v_use ->
err st pos
"affine 값을 capture한 클로저는 fn 자리에 넘길 수 없습니다 (affine fn이어야 합니다)"
| _ -> ())
args;
match info with
| Some { f_ret = Some t; _ } -> { v_affine = ty_affine st t; v_use = false }
| _ -> v_copy
and callee_info st callee =
match callee with
| E_ident (n, _) when find st n = None -> Hashtbl.find_opt st.fns n
| E_inst { callee = E_ident (n, _); _ } when find st n = None ->
Hashtbl.find_opt st.fns n
| E_field { obj = E_ident (o, _); name; _ } -> (
match find st o with
| Some _ -> (
(* 값의 메서드: 타입을 모르면 넘어간다. capability 메서드는 아래에서 *)
match
Hashtbl.fold
(fun _ methods acc ->
match acc with
| Some _ -> acc
| None -> List.assoc_opt name methods)
st.meths None
with
| Some f -> Some f
| None -> None)
(* 모듈 별칭을 통한 호출: 가져온 함수는 "Alias.f" 키로 들어와 있다 *)
| None -> Hashtbl.find_opt st.fns (o ^ "." ^ name))
| _ -> None
(* ------------------------------------------------------------------ *)
(* 선언 *)
(* ------------------------------------------------------------------ *)
let check_fn st (d : fn_decl) =
match d.fn_body with
| None -> ()
| Some body ->
st.scopes <- [];
Hashtbl.reset st.moved;
st.depth <- 0;
st.frames <- [];
push st;
List.iter
(fun p ->
let affine = ty_affine st p.p_ty in
(* 무표기 = 빌림. own만이 소유 이전이다. *)
let use_ = affine && not p.p_own in
ignore (add st p.p_name ~affine ~use:use_ ~mut_:p.p_mut))
d.fn_params;
let r = walk_block st (Move "반환할 수") body in
(match d.fn_ret with
| Some (T_fn { affine = false; _ }) when r.v_affine && not r.v_use ->
err st d.fn_pos
(Printf.sprintf
"%s이(가) affine 값을 capture한 클로저를 fn 타입으로 반환합니다 (affine fn이어야 합니다)"
d.fn_name)
| _ -> ());
pop st
let check ?(imports : item list = []) (m : modul) : error list =
let st =
{
aff = Hashtbl.create 16;
fns = Hashtbl.create 16;
meths = Hashtbl.create 16;
scopes = [];
moved = Hashtbl.create 16;
next_id = 0;
depth = 0;
frames = [];
cl_expect = None;
errors = [];
}
in
derive_affinity st (imports @ m.items);
List.iter
(fun it ->
match it with
| I_fn { decl; _ } ->
Hashtbl.replace st.fns decl.fn_name
{ f_params = decl.fn_params; f_ret = decl.fn_ret }
| I_capability { name; methods; _ } ->
Hashtbl.replace st.meths name
(List.map
(fun d ->
(d.fn_name, { f_params = d.fn_params; f_ret = d.fn_ret }))
methods)
| _ -> ())
(imports @ m.items);
List.iter
(fun it ->
match it with
| I_fn { decl; _ } -> check_fn st decl
(* 테스트도 같은 검사를 받는다 *)
| I_test { body; _ } ->
st.scopes <- [];
push st;
ignore (walk_block st (Move "반환할 수") body);
pop st
| _ -> ())
m.items;
List.sort
(fun a b ->
compare
(a.pos.Token.line, a.pos.Token.col)
(b.pos.Token.line, b.pos.Token.col))
(List.rev st.errors)