move: move/affinity 검사 — v0 fast path 완성
보안 정리 (ii) — safe code에서 capability는 복제·위조되지 않는다 — 를 코드로 닫는다. 검사는 전부 함수 로컬 데이터플로우이고 전역 분석이 없다. affinity의 뿌리는 capability다. 필드로 가진 타입은 전이적으로 affine이며 고정점까지 돌려 상호 재귀 타입도 유도한다. 이 전이가 없으면 wrapper 하나를 복사해 capability가 사실상 복제되므로 정리가 깨진다. copyable 선언과 affine 필드의 공존은 오류다. 구현한 규칙: - affine 값은 소유 자리로 갈 때 move된다(own 파라미터, 반환, struct 저장, 컨테이너 삽입, let 바인딩, by-move capture). moved 이후 사용은 오류이고 진단이 어디서 소비됐는지를 말한다 - 분기 병합은 보수적 합집합. 한 분기에서라도 moved면 병합 이후 moved - 빌린 값은 탈출하지 못한다: 반환, struct 저장, 소유 자리로 넘기기 전부 거부 - use의 전염: 빌린 값을 capture한 클로저는 그 자체가 빌린 값이라 소유 자리로 갈 수 없다. 별도의 nonescaping 개념 없이 use 규칙 하나로 닫힌다 - callable affinity: affine 값을 capture한 클로저는 affine fn이며 fn 자리에 갈 수 없다 자율 결정 둘: - 클로저는 mut 바인딩을 capture할 수 없다. spawn만 막는 특수 규칙 대신 일반 규칙으로 뒀다 — v0에 참조가 없으므로 별칭도 조용한 복사도 만들 수 없고, spawn 제한은 이 규칙의 특수 사례가 된다 - v0에 부분 move는 없다. 필드 접근은 빌림이고 결과도 빌린 값이다. affine 필드만 꺼내려면 부분 move 상태 추적이 필요한데 v0가 살 복잡도가 아니다 05를 자족적으로 다시 썼다. affinity의 뿌리가 capability라 자원 타입을 모듈 안에서 정의해야 검사기가 affine임을 유도할 수 있다. 외부 타입은 affine임을 증명할 수 없으므로 copyable로 본다. 이로써 fast path(L0 parse / L1 type·effect·capability·ownership)가 완성됐다. cool check가 처음으로 성공을 선언한다 — 01~04, 06, 07이 exit 0으로 통과한다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
+471
@@ -0,0 +1,471 @@
|
||||
(* 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;
|
||||
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 { name; args; _ } ->
|
||||
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 (m : modul) =
|
||||
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
|
||||
| _ -> ())
|
||||
m.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)
|
||||
| _ -> ())
|
||||
m.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
|
||||
| _ -> ())
|
||||
m.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
|
||||
| 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;
|
||||
List.iter
|
||||
(fun (n, ann) ->
|
||||
let affine = match ann with Some t -> ty_affine st t | None -> false in
|
||||
ignore (add st n ~affine ~use:false ~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
|
||||
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)
|
||||
| None -> None)
|
||||
| _ -> 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 (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 = [];
|
||||
errors = [];
|
||||
}
|
||||
in
|
||||
derive_affinity st m;
|
||||
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)
|
||||
| _ -> ())
|
||||
m.items;
|
||||
List.iter
|
||||
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ())
|
||||
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)
|
||||
Reference in New Issue
Block a user