Files
coollang/lib/move.ml
T
coolguyandClaude Opus 5 78ef07d2ee panic: panic/Never와 내장 테스트 — 문법을 먼저 고치고 대조 장치가 파서를 지적했다
순서가 요점이다. 문법에 test_decl과 panic_expr을 넣고 파서는 안 고친 채로
대조 장치를 돌렸더니 즉시 잡혔다:

  문장 300개 중 파서가 거부한 것 140개
  [1] 선언 (fn, struct, enum, capability, const)이(가) 필요합니다 — test 발견

파서를 따라가게 하니 다시 0건. 문법과 구현이 어긋나는 상태가 관측 가능한
것이 되었다는 뜻이다.

panic:
- 키워드다. prelude가 없어 함수로 두면 쓸 때마다 import해야 한다
- effect가 아니다. 경계 검사 하나에 {Panic}이 호출자 전부로 전염되면
  effect 절은 신호가 아니라 잡음이 된다
- Never는 어떤 타입 자리에도 놓인다. 없으면 panic을 match 팔에서 못 쓴다
- 언어 수준 recover 없음. 되감기 없음. 자원 해제 여부는 열어둔다
- 0으로 나누기, assert 실패가 이 하나로 모인다

test:
- 파라미터가 없어 capability를 받을 수 없고, 만들 문법도 없다. 그래서
  effect-free임이 증명된다 — 관례가 아니라 검사다. 시험해 보니 실제로
  "테스트는 effect를 수행할 수 없습니다"로 거부한다
- 일반 코드와 같은 타입/effect/move 검사를 받는다
- interface hash에서 제외 — 테스트를 고쳤다고 downstream이 재검사되면 안 된다
- 격리는 런타임의 일이다. 하나가 죽어도 나머지는 돈다

assert는 std/test.cool에 coollang으로 쓰였다 — panic 위의 설탕임이 코드로
보이고, std에서 본문이 있는 첫 함수가 됐다. 그 바람에 std/런타임 양방향
테스트가 걸렸고(본문 있는 함수에 런타임 구현을 요구했다), 그 구분을 넣었다.

samples/app/config.cool에 첫 테스트 넷.

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

487 lines
17 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;
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
(* panic은 돌아오지 않지만 메시지 식은 계산된다. 메시지는 빌려 쓴다 *)
| E_panic { 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;
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)
(* 모듈 별칭을 통한 호출: 가져온 함수는 "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 = [];
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)