From 1c4f46e5e56a3a7a4bdbfe58059c53386a1c94b9 Mon Sep 17 00:00:00 2001 From: coolguy Date: Sun, 30 Aug 2026 14:58:05 +0900 Subject: [PATCH] =?UTF-8?q?interp:=20=EC=96=87=EC=9D=80=20typed=20IR?= =?UTF-8?q?=EA=B3=BC=20=ED=8A=B8=EB=A6=AC=20=EC=9B=8C=ED=82=B9=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=94=84=EB=A6=AC=ED=84=B0=20=E2=80=94=20coolc=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 문서에만 있던 실행 의미가 코드가 된다. - ir.ml: AST를 얇은 IR로 낮춘다. `?`는 Result에 대한 match로 펼쳐지고, 타입 인자는 사라지며(단형화 없음), 한정 이름은 하나의 이름으로 접힌다. 이름은 낮추기 시점에 분류된다 — 실행 중에 "지역인가 전역인가"를 다시 묻지 않는다. - interp.ml: 검사하지 않는 인터프리터. 여기 도달한 프로그램은 이미 타입, effect, capability, ownership 검사를 통과했고, 같은 질문을 두 번 묻는 것은 두 번째 진실을 만드는 일이다. 권한의 유일한 출처는 런타임이다. 소스에는 capability를 만드는 문법이 없고, main은 자기가 선언한 것만 받는다. 파라미터에서 Console을 지우면 출력할 방법이 프로그램 안에 없다 — 보안 정리 (i)의 실행 시점 대응물. TaskScope의 뿌리도 같은 이유로 런타임이 준다. scope의 v0 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다. samples/run/은 이제 "검사를 통과한다"가 아니라 "이 값을 낸다"까지 말하고, test/가 같은 것을 검사한다. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E --- bin/main.ml | 10 +- docs/thesis.md | 7 +- lib/driver.ml | 7 +- lib/interp.ml | 340 +++++++++++++++++++++++++++++++++++++++ lib/ir.ml | 304 ++++++++++++++++++++++++++++++++++ lib/session.ml | 44 +++++ samples/README.md | 9 ++ samples/run/control.cool | 55 +++++++ samples/run/hello.cool | 28 ++++ test/test_coollang.ml | 115 +++++++++++++ 10 files changed, 913 insertions(+), 6 deletions(-) create mode 100644 lib/interp.ml create mode 100644 lib/ir.ml create mode 100644 samples/run/control.cool create mode 100644 samples/run/hello.cool diff --git a/bin/main.ml b/bin/main.ml index 73c8d0f..4b6299b 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -76,7 +76,15 @@ let () = match List.tl argv with | "check" :: files -> check_graph files | [ "iface"; file ] -> dump_iface file - | [ "run"; file ] -> report (Coollang.Driver.run file) + | [ "run"; file ] -> ( + let st = Coollang.Session.create ~root:(Filename.dirname file) () in + match Coollang.Session.run st file with + | Ok out -> + print_string out; + 0 + | Error e -> + prerr_endline (Coollang.Session.string_of_error e); + 1) | [ "tokens"; file ] -> dump_tokens file | [ "ast"; file ] -> dump_ast file | [ "deps"; file ] -> dump_deps file diff --git a/docs/thesis.md b/docs/thesis.md index cd3c93a..e0e8e51 100644 --- a/docs/thesis.md +++ b/docs/thesis.md @@ -290,8 +290,13 @@ Generics (철학 2에서 파생): - effect 변수 (effect 다형성) - interface artifact + hash 기반 incremental invalidation - coolc check -- 얇은 typed IR + tree-walking interpreter (coolc run 대용) +- 얇은 typed IR + tree-walking interpreter (coolc run) ※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게. + ※ 실행 시점에도 권한의 출처는 런타임 하나다. main이 선언한 capability만 + 넘어가고, 소스에는 capability를 만드는 문법이 없다 — 보안 정리 (i)의 + 실행 시점 대응물. TaskScope의 뿌리도 같은 이유로 런타임이 준다. + ※ v0의 scope 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다 — + 순서가 반대면 취소와 전파를 나중에 끼워 넣게 된다. ※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상 제외 (아키텍처 검증 후 얹어도 되는 것): diff --git a/lib/driver.ml b/lib/driver.ml index 032a40a..7abbf1b 100644 --- a/lib/driver.ml +++ b/lib/driver.ml @@ -2,7 +2,9 @@ parse -> name resolution -> type check -> effect/capability check -> interface artifact + hash -> (coolc run 시) 얇은 typed IR -> interpreter - 현재 구현된 단계: 어휘 분석, 구문 분석. *) + 이 파일은 단일 파일 도구(tokens/ast/deps)만 남았다. 모듈 그래프를 다루는 + check와 run은 Session이 소유한다 — import를 따라가야 하는 순간부터 + "파일 하나"는 더 이상 단위가 아니다. *) type error = { file : string; line : int; col : int; message : string } @@ -103,6 +105,3 @@ let check (files : string list) : (unit, error list) result = |> List.concat in if errors <> [] then Error errors else Ok () - -let run (file : string) : (unit, error list) result = - Error [ { file; line = 0; col = 0; message = "interpreter가 아직 구현되지 않았습니다" } ] diff --git a/lib/interp.ml b/lib/interp.ml new file mode 100644 index 0000000..d6f4c6e --- /dev/null +++ b/lib/interp.ml @@ -0,0 +1,340 @@ +(* 트리 워킹 인터프리터. + + 여기 도달한 프로그램은 이미 타입, effect, capability, ownership 검사를 + 통과했다. 그러므로 이 파일은 검사하지 않는다 — 검사기가 이미 답한 질문을 + 실행 시점에 다시 묻는 것은 두 번째 진실을 만드는 일이다. + + 실행 시점 오류로 남는 것은 검사기가 원리적으로 못 잡는 것뿐이다: + 0으로 나누기, 리스트 범위, 그리고 아직 없는 표준 라이브러리 이름. + + 권한의 유일한 출처는 런타임이다. 소스에는 capability를 만드는 문법이 없고, + main은 자기가 선언한 capability만 받는다. 선언하지 않은 권한은 프로그램 + 어디에도 존재하지 않는다 — 보안 정리 (i)의 실행 시점 대응물이다. *) + +type value = + | VUnit + | VInt of int + | VBool of bool + | VStr of string + | VList of value list + | VStruct of string * (string * value ref) list + | VEnum of string * string * value list (* enum, variant, 인자 *) + | VClosure of { params : string list; body : Ir.t; env : env } + | VFn of Ir.fn + | VCtor of string * string * int (* 아직 인자를 안 받은 생성자 *) + | VBuiltin of string + | VNative of (value list -> value) + | VCap of string * (string * (value list -> value)) list + | VScope of string + +and env = (string * value ref) list list + +exception Return_exc of value +exception Fail of Token.pos * string + +let fail pos msg = raise (Fail (pos, msg)) + +let rec show = function + | VUnit -> "unit" + | VInt n -> string_of_int n + | VBool b -> if b then "true" else "false" + | VStr s -> s + | VList xs -> "[" ^ String.concat ", " (List.map show xs) ^ "]" + | VStruct (n, fs) -> + n ^ "{" + ^ String.concat ", " (List.map (fun (k, v) -> k ^ ": " ^ show !v) fs) + ^ "}" + | VEnum (_, v, []) -> v + | VEnum (_, v, args) -> + v ^ "(" ^ String.concat ", " (List.map show args) ^ ")" + | VClosure _ | VFn _ | VBuiltin _ | VCtor _ | VNative _ -> "" + | VCap (n, _) -> "" + | VScope n -> "" + +let rec eq a b = + match (a, b) with + | VInt x, VInt y -> x = y + | VBool x, VBool y -> x = y + | VStr x, VStr y -> x = y + | VUnit, VUnit -> true + | VList x, VList y -> List.length x = List.length y && List.for_all2 eq x y + | VEnum (_, v1, a1), VEnum (_, v2, a2) -> + v1 = v2 && List.length a1 = List.length a2 && List.for_all2 eq a1 a2 + | _ -> false + +(* ------------------------------------------------------------------ *) +(* 환경 *) +(* ------------------------------------------------------------------ *) + +let lookup (env : env) n = + let rec go = function + | [] -> None + | s :: r -> ( + match List.assoc_opt n s with Some v -> Some v | None -> go r) + in + go env + +let bind (env : env) n v : env = + match env with s :: r -> ((n, ref v) :: s) :: r | [] -> [ [ (n, ref v) ] ] + +(* ------------------------------------------------------------------ *) +(* 런타임이 제공하는 것 *) +(* ------------------------------------------------------------------ *) + +let out = Buffer.create 1024 + +let root_capability name : value option = + match name with + | "Console" -> + Some + (VCap + ( "Console", + [ + ( "print", + fun args -> + List.iter (fun v -> Buffer.add_string out (show v)) args; + Buffer.add_char out '\n'; + VUnit ); + ] )) + | "TaskScope" -> + (* 루트 스코프. 구조적 동시성의 뿌리도 런타임이 준다 — 프로그램이 + 스스로 만들 수 있으면 부모 없는 작업이 생긴다. *) + Some (VScope "root") + | _ -> None + +let builtin pos name (args : value list) : value = + match (name, args) with + | "String.concat", [ VStr a; VStr b ] -> VStr (a ^ b) + | "String.len", [ VStr a ] -> VInt (String.length a) + | "Int.show", [ VInt n ] -> VStr (string_of_int n) + | "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) + +(* ------------------------------------------------------------------ *) +(* 실행 *) +(* ------------------------------------------------------------------ *) + +type st = { prog : Ir.program } + +let rec eval st (env : env) (e : Ir.t) : value = + match e with + | Ir.I_unit -> VUnit + | Ir.I_lit (Ast.L_int n) -> VInt (int_of_string n) + | Ir.I_lit (Ast.L_str s) -> VStr s + | Ir.I_lit (Ast.L_bool b) -> VBool b + | Ir.I_ref (k, pos) -> eval_ref st env k pos + | Ir.I_list xs -> VList (List.map (eval st env) xs) + | Ir.I_make (n, fields) -> + VStruct (n, List.map (fun (k, e) -> (k, ref (eval st env e))) fields) + | Ir.I_closure c -> VClosure { params = c.c_params; body = c.c_body; env } + | Ir.I_if { cond; then_; else_ } -> ( + match eval st env cond with + | VBool true -> eval st ([] :: env) then_ + | _ -> eval st ([] :: env) else_) + | Ir.I_match { scrutinee; arms; pos } -> + let v = eval st env scrutinee in + let rec go = function + | [] -> fail pos "match에서 어떤 팔도 맞지 않았습니다" + | (p, body) :: rest -> ( + match match_pat v p with + | None -> go rest + | Some binds -> + let env = + List.fold_left (fun e (n, v) -> bind e n v) ([] :: env) binds + in + eval st env body) + in + go arms + | Ir.I_scope { name; body; _ } -> + (* v0의 실행 의미: 자식 작업은 순차로 돈다. 블록을 나가는 것이 join이다. + 구조가 먼저고 병렬성은 그 위의 최적화다 — 순서가 반대면 취소와 + 전파를 나중에 끼워 넣게 된다. *) + let env = bind ([] :: env) name (VScope name) in + eval st env body + | Ir.I_seq (stmts, tail) -> + let env = List.fold_left (fun env s -> exec st env s) ([] :: env) stmts in + eval st env tail + | Ir.I_call { callee; args; pos } -> + let f = eval st env callee in + let args = List.map (eval st env) args in + apply st pos f args + | Ir.I_field { obj; name; pos } -> ( + match eval st env obj with + | VStruct (sn, fields) -> ( + match List.assoc_opt name fields with + | Some r -> !r + | None -> fail pos (Printf.sprintf "%s에 %s 필드가 없습니다" sn name)) + | VCap (cn, meths) -> ( + (* capability 메서드는 값을 통해서만 나온다. 여기가 권한이 코드로 + 흐르는 유일한 통로다. *) + match List.assoc_opt name meths with + | Some f -> VNative f + | None -> + fail pos (Printf.sprintf "capability %s에 %s이(가) 없습니다" cn name)) + | VScope _ when name = "spawn" -> + VNative + (fun args -> + match args with + | [ f ] -> apply st pos f [] + | _ -> fail pos "spawn은 함수 하나를 받습니다") + | other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other))) + | Ir.I_unary (op, e, pos) -> ( + match (op, eval st env e) with + | Ast.U_not, VBool b -> VBool (not b) + | Ast.U_neg, VInt n -> VInt (-n) + | _ -> fail pos "단항 연산자의 피연산자가 맞지 않습니다") + | Ir.I_binary (op, a, b, pos) -> eval_binary st env op a b pos + +and eval_ref st env k pos = + match k with + | Ir.R_local n -> ( + match lookup env n with + | Some r -> !r + | None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n)) + | Ir.R_ctor (enum, name) -> ( + match Hashtbl.find_opt st.prog.Ir.ctors name with + | Some (_, 0) -> VEnum (enum, name, []) + | Some (_, n) -> VCtor (enum, name, n) + | None -> VEnum (enum, name, [])) + | Ir.R_global n -> ( + match Hashtbl.find_opt st.prog.Ir.fns n with + | Some f -> VFn f + | None -> ( + match Hashtbl.find_opt st.prog.Ir.consts n with + | Some e -> eval st [ [] ] e + | None -> fail pos (Printf.sprintf "%s을(를) 찾을 수 없습니다" n))) + | Ir.R_builtin n -> VBuiltin n + +and apply st pos f args = + match f with + | VFn fn -> ( + let env = [ List.map2 (fun p a -> (p, ref a)) fn.Ir.fn_params args ] in + try eval st env fn.Ir.fn_body with Return_exc v -> v) + | VClosure { params; body; env } -> ( + let env = List.map2 (fun p a -> (p, ref a)) params args :: env in + try eval st env body with Return_exc v -> v) + | VCtor (enum, name, _) -> VEnum (enum, name, args) + (* 고차 builtin은 여기서 처리한다 — apply를 다시 부를 수 있어야 하므로 *) + | VBuiltin "List.each" -> ( + match args with + | [ VList xs; f ] -> + List.iter (fun x -> ignore (apply st pos f [ x ])) xs; + VUnit + | _ -> fail pos "List.each는 리스트와 함수를 받습니다") + | VBuiltin "List.map" -> ( + match args with + | [ VList xs; f ] -> VList (List.map (fun x -> apply st pos f [ x ]) xs) + | _ -> fail pos "List.map은 리스트와 함수를 받습니다") + | VBuiltin n -> builtin pos n args + | VNative f -> f args + | other -> fail pos (Printf.sprintf "%s은(는) 부를 수 없습니다" (show other)) + +and exec st env (s : Ir.stmt) : env = + match s with + | Ir.S_do e -> + ignore (eval st env e); + env + | Ir.S_return (e, _) -> raise (Return_exc (eval st env e)) + | Ir.S_let (p, e, pos) -> ( + let v = eval st env e in + match match_pat v p with + | None -> fail pos "let 패턴이 값과 맞지 않습니다" + | Some binds -> List.fold_left (fun e (n, v) -> bind e n v) env binds) + | Ir.S_assign { place; value; pos } -> ( + let v = eval st env value in + match place with + | Ir.I_ref (Ir.R_local n, _) -> ( + match lookup env n with + | Some r -> + r := v; + env + | None -> fail pos (Printf.sprintf "%s이(가) 묶여 있지 않습니다" n)) + | Ir.I_field { obj; name; _ } -> ( + match eval st env obj with + | VStruct (_, fields) -> ( + match List.assoc_opt name fields with + | Some r -> + r := v; + env + | None -> fail pos (Printf.sprintf "%s 필드가 없습니다" name)) + | _ -> fail pos "필드에 대입할 수 없습니다") + | _ -> fail pos "대입할 수 없는 자리입니다") + +and match_pat v (p : Ir.pat) : (string * value) list option = + match (p, v) with + | Ir.IP_wild, _ -> Some [] + | Ir.IP_bind n, _ -> Some [ (n, v) ] + | Ir.IP_lit (Ast.L_int n), VInt m -> + if int_of_string n = m then Some [] else None + | Ir.IP_lit (Ast.L_str s), VStr t -> if s = t then Some [] else None + | Ir.IP_lit (Ast.L_bool b), VBool c -> if b = c then Some [] else None + | Ir.IP_ctor (name, ps), VEnum (_, vn, args) -> + if name <> vn || List.length ps <> List.length args then None + else + List.fold_left2 + (fun acc p a -> + match (acc, match_pat a p) with + | Some xs, Some ys -> Some (xs @ ys) + | _ -> None) + (Some []) ps args + | _ -> None + +and eval_binary st env op a b pos = + match op with + (* 단축 평가. 오른쪽을 먼저 계산하면 && 의 의미가 달라진다 *) + | Ast.B_and -> ( + match eval st env a with VBool false -> VBool false | _ -> eval st env b) + | Ast.B_or -> ( + match eval st env a with VBool true -> VBool true | _ -> eval st env b) + | _ -> ( + let x = eval st env a and y = eval st env b in + match (op, x, y) with + | Ast.B_eq, _, _ -> VBool (eq x y) + | Ast.B_ne, _, _ -> VBool (not (eq x y)) + | Ast.B_lt, VInt m, VInt n -> VBool (m < n) + | Ast.B_le, VInt m, VInt n -> VBool (m <= n) + | Ast.B_gt, VInt m, VInt n -> VBool (m > n) + | Ast.B_ge, VInt m, VInt n -> VBool (m >= n) + | Ast.B_add, VInt m, VInt n -> VInt (m + n) + | Ast.B_sub, VInt m, VInt n -> VInt (m - n) + | Ast.B_mul, VInt m, VInt n -> VInt (m * n) + | Ast.B_div, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다" + | Ast.B_div, VInt m, VInt n -> VInt (m / n) + | Ast.B_rem, VInt _, VInt 0 -> fail pos "0으로 나눌 수 없습니다" + | Ast.B_rem, VInt m, VInt n -> VInt (m mod n) + | _ -> fail pos "연산자의 피연산자 타입이 맞지 않습니다") + +(* ------------------------------------------------------------------ *) +(* 진입 *) +(* ------------------------------------------------------------------ *) + +(* main이 선언한 capability만 런타임이 넘긴다. 선언하지 않은 권한은 + 프로그램 안에 존재하지 않는다. *) +let run (prog : Ir.program) (main_params : (string * string) list) : + (string, Token.pos * string) result = + Buffer.clear out; + let st = { prog } in + match Hashtbl.find_opt prog.Ir.fns "main" with + | None -> Error (Token.{ line = 0; col = 0 }, "main 함수가 없습니다") + | Some fn -> ( + let args = + List.map + (fun (_, tyname) -> + match root_capability tyname with + | Some v -> Ok v + | None -> + Error (Printf.sprintf "런타임이 %s capability를 제공하지 않습니다" tyname)) + main_params + in + match List.find_opt Result.is_error args with + | Some (Error m) -> Error (Token.{ line = 0; col = 0 }, m) + | _ -> ( + let args = List.map Result.get_ok args in + try + ignore (apply st Token.{ line = 0; col = 0 } (VFn fn) args); + Ok (Buffer.contents out) + with + | Fail (pos, msg) -> Error (pos, msg) + | Return_exc _ -> Ok (Buffer.contents out))) diff --git a/lib/ir.ml b/lib/ir.ml new file mode 100644 index 0000000..9d19a18 --- /dev/null +++ b/lib/ir.ml @@ -0,0 +1,304 @@ +(* 얇은 typed IR. + + 왜 AST를 직접 해석하지 않는가 — 표면 문법이 실행 의미에 닿지 않는다는 것을 + 구조로 강제하기 위해서다. v1이 백엔드를 무엇으로 바꾸든 소비하는 것은 이 + IR이고, 문법을 고쳐도 여기가 그대로면 실행 의미는 그대로다. + + 얇다는 것의 뜻: 새 개념을 만들지 않는다. 검사 단계가 이미 답한 질문을 + 다시 묻지 않는다 — 여기 도달한 프로그램은 타입, effect, capability, + ownership 검사를 모두 통과했다. 그래서 IR에는 타입 검사가 없다. + + 낮추기에서 사라지는 것: + - E_try: Result에 대한 match로 펼친다. `?`는 설탕이다 + - E_inst: 타입 인자는 실행에 영향이 없다 (단형화 없음, 값 표현이 같다) + - 한정 이름: "Alias.f" 하나의 이름으로 평탄화된다 (검사 단계와 같은 규칙) *) + +type pos = Token.pos + +(* 이름은 낮추기 시점에 분류된다. 실행 중에 "이게 지역인가 전역인가"를 + 다시 묻지 않는다. *) +type ref_kind = + | R_local of string + | R_global of string (* 이 프로그램의 함수 *) + | R_ctor of string * string (* enum 이름, variant 이름 *) + | R_builtin of string (* String.concat 등 런타임 제공 *) + +type pat = + | IP_wild + | IP_lit of Ast.lit + | IP_bind of string + | IP_ctor of string * pat list (* variant 이름 *) + +type t = + | I_unit + | I_lit of Ast.lit + | I_ref of ref_kind * pos + | I_list of t list + | I_make of string * (string * t) list (* struct 생성 *) + | I_closure of closure + | I_if of { cond : t; then_ : t; else_ : t } + | I_match of { scrutinee : t; arms : (pat * t) list; pos : pos } + | I_scope of { name : string; body : t; pos : pos } + | I_seq of stmt list * t (* 블록: 문 나열 + 꼬리 값 *) + | I_call of { callee : t; args : t list; pos : pos } + | I_field of { obj : t; name : string; pos : pos } + | I_unary of Ast.unop * t * pos + | I_binary of Ast.binop * t * t * pos + +and stmt = + | S_let of pat * t * pos + | S_assign of { place : t; value : t; pos : pos } + | S_return of t * pos + | S_do of t + +and closure = { c_params : string list; c_body : t; c_pos : pos } + +type fn = { fn_name : string; fn_params : string list; fn_body : t } + +type program = { + fns : (string, fn) Hashtbl.t; + (* variant 이름 -> (enum 이름, 인자 개수) *) + ctors : (string, string * int) Hashtbl.t; + caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *) + consts : (string, t) Hashtbl.t; +} + +(* ------------------------------------------------------------------ *) +(* 낮추기 *) +(* ------------------------------------------------------------------ *) + +let qual modl name = match modl with Some a -> a ^ "." ^ name | None -> name + +(* 지역 이름 스택. 검사 단계가 아니라 분류만 한다 — 여기서 못 찾은 이름은 + 전역이거나 생성자이거나 런타임 제공이다. *) +type lctx = { prog : program; mutable locals : string list list } + +let lpush c = c.locals <- [] :: c.locals +let lpop c = match c.locals with _ :: r -> c.locals <- r | [] -> () + +let lbind c n = + match c.locals with + | s :: r -> c.locals <- (n :: s) :: r + | [] -> c.locals <- [ [ n ] ] + +let is_local c n = List.exists (fun s -> List.mem n s) c.locals + +let builtins = + [ + "String.concat"; + "String.len"; + "Int.show"; + "Bool.show"; + "List.len"; + "List.each"; + "List.map"; + "print"; + ] + +let classify c name = + if is_local c name then R_local name + else + match Hashtbl.find_opt c.prog.ctors name with + | Some (enum, _) -> R_ctor (enum, name) + | None -> + if Hashtbl.mem c.prog.fns name || Hashtbl.mem c.prog.consts name then + R_global name + else R_builtin name + +let rec lower_pat c (p : Ast.pattern) : pat = + match p with + | Ast.P_wild _ -> IP_wild + | Ast.P_lit (l, _) -> IP_lit l + | Ast.P_bind (n, _) -> + (* 인자 없는 생성자는 이름만 쓴다. 바인딩과 구별은 여기서 끝난다. *) + if Hashtbl.mem c.prog.ctors n then IP_ctor (n, []) + else ( + lbind c n; + IP_bind n) + | Ast.P_ctor { modl; name; args; _ } -> + IP_ctor (qual modl name, List.map (lower_pat c) args) + +let rec lower c (e : Ast.expr) : t = + match e with + | Ast.E_lit (l, _) -> I_lit l + | Ast.E_ident (n, pos) -> I_ref (classify c n, pos) + | Ast.E_list (xs, _) -> I_list (List.map (lower c) xs) + | Ast.E_struct { name; fields; _ } -> + I_make (name, List.map (fun (n, e) -> (n, lower c e)) fields) + | Ast.E_closure cl -> + lpush c; + List.iter (fun (n, _) -> lbind c n) cl.cl_params; + let body = lower_block c cl.cl_body in + lpop c; + I_closure + { + c_params = List.map fst cl.cl_params; + c_body = body; + c_pos = cl.cl_pos; + } + | Ast.E_if { cond; then_; else_; _ } -> + let cond = lower c cond in + lpush c; + let t = lower_block c then_ in + lpop c; + let e = + match else_ with + | None -> I_unit + | Some e -> + lpush c; + let v = lower c e in + lpop c; + v + in + I_if { cond; then_ = t; else_ = e } + | Ast.E_match { scrutinee; arms; pos } -> + let s = lower c scrutinee in + let arms = + List.map + (fun (a : Ast.arm) -> + lpush c; + let p = lower_pat c a.arm_pat in + let b = lower c a.arm_body in + lpop c; + (p, b)) + arms + in + I_match { scrutinee = s; arms; pos } + | Ast.E_scope { name; body; pos; _ } -> + lpush c; + lbind c name; + let b = lower_block c body in + lpop c; + I_scope { name; body = b; pos } + | Ast.E_block b -> + lpush c; + let v = lower_block c b in + lpop c; + v + | Ast.E_call { callee; args; pos } -> + I_call { callee = lower c callee; args = List.map (lower c) args; pos } + | Ast.E_field { obj; name; pos } -> ( + (* Alias.f / String.concat 처럼 이름공간 접근은 하나의 이름으로 접는다. + 값의 필드 접근과 구별되는 지점은 obj가 지역 이름이 아닌 것뿐이다. *) + match obj with + | Ast.E_ident (o, _) when not (is_local c o) -> + I_ref (classify c (o ^ "." ^ name), pos) + | _ -> I_field { obj = lower c obj; name; pos }) + | Ast.E_inst { callee; _ } -> lower c callee + | Ast.E_try { inner; pos } -> + (* `?`는 설탕이다: Ok(v) => v, Err(e) => return Err(e) *) + I_match + { + scrutinee = lower c inner; + arms = + [ + (IP_ctor ("Ok", [ IP_bind "?v" ]), I_ref (R_local "?v", pos)); + ( IP_ctor ("Err", [ IP_bind "?e" ]), + I_seq + ( [ + S_return + ( I_call + { + callee = I_ref (R_ctor ("Result", "Err"), pos); + args = [ I_ref (R_local "?e", pos) ]; + pos; + }, + pos ); + ], + I_unit ) ); + ]; + pos; + } + | Ast.E_unary { op; operand; pos } -> I_unary (op, lower c operand, pos) + | Ast.E_binary { op; lhs; rhs; pos } -> + I_binary (op, lower c lhs, lower c rhs, pos) + +and lower_block c (b : Ast.block) : t = + let rec go = function + | [] -> I_unit + | [ Ast.S_expr e ] -> lower c e + | s :: rest -> ( + let s = lower_stmt c s in + let tail = go rest in + match tail with + | I_seq (ss, t) -> I_seq (s :: ss, t) + | t -> I_seq ([ s ], t)) + in + go b.stmts + +and lower_stmt c (s : Ast.stmt) : stmt = + match s with + | Ast.S_let { pat; value; pos; _ } -> + let v = lower c value in + (* 값을 먼저 낮춘다 — 바인딩은 그 뒤에야 보인다 *) + S_let (lower_pat c pat, v, pos) + | Ast.S_return { value; pos } -> + S_return ((match value with Some e -> lower c e | None -> I_unit), pos) + | Ast.S_assign { place; value; pos } -> + S_assign { place = lower c place; value = lower c value; pos } + | Ast.S_expr e -> S_do (lower c e) + +let of_module ?(imports : Ast.item list = []) (m : Ast.modul) : program = + let prog = + { + fns = Hashtbl.create 32; + ctors = Hashtbl.create 32; + caps = Hashtbl.create 8; + consts = Hashtbl.create 8; + } + in + let items = imports @ m.items in + (* 1차: 이름부터. 낮추기가 이름을 분류하려면 전체가 먼저 보여야 한다. *) + List.iter + (fun (it : Ast.item) -> + match it with + | Ast.I_enum { name; variants; _ } -> + List.iter + (fun (v : Ast.variant) -> + Hashtbl.replace prog.ctors v.v_name (name, List.length v.v_args)) + variants + | Ast.I_capability { name; methods; _ } -> + Hashtbl.replace prog.caps name + (List.map (fun (d : Ast.fn_decl) -> d.fn_name) methods) + | Ast.I_fn { decl; _ } when decl.fn_body <> None -> + Hashtbl.replace prog.fns decl.fn_name + { fn_name = decl.fn_name; fn_params = []; fn_body = I_unit } + | Ast.I_const { name; _ } -> Hashtbl.replace prog.consts name I_unit + | _ -> ()) + items; + (* builtin 생성자도 IR이 알아야 한다 *) + List.iter + (fun (v, e, n) -> Hashtbl.replace prog.ctors v (e, n)) + [ + ("Ok", "Result", 1); + ("Err", "Result", 1); + ("Some", "Option", 1); + ("None", "Option", 0); + ]; + (* 2차: 본문 *) + let c = { prog; locals = [] } in + List.iter + (fun (it : Ast.item) -> + match it with + | Ast.I_fn { decl; _ } -> ( + match decl.fn_body with + | None -> () + | Some body -> + c.locals <- []; + lpush c; + List.iter (fun (p : Ast.param) -> lbind c p.p_name) decl.fn_params; + let b = lower_block c body in + lpop c; + Hashtbl.replace prog.fns decl.fn_name + { + fn_name = decl.fn_name; + fn_params = + List.map (fun (p : Ast.param) -> p.p_name) decl.fn_params; + fn_body = b; + }) + | Ast.I_const { name; value; _ } -> + c.locals <- []; + Hashtbl.replace prog.consts name (lower c value) + | _ -> ()) + items; + prog diff --git a/lib/session.ml b/lib/session.ml index a90968e..145e48b 100644 --- a/lib/session.ml +++ b/lib/session.ml @@ -179,3 +179,47 @@ let errors st = compare (a.file, a.line, a.col) (b.file, b.line, b.col)) let find st path = Hashtbl.find_opt st.modules path + +(* ------------------------------------------------------------------ *) +(* 실행 *) +(* ------------------------------------------------------------------ *) + +(* main이 선언한 파라미터의 타입 이름을 뽑는다. 런타임은 이 목록만 보고 + 권한을 만든다 — 선언하지 않은 capability는 프로그램에 존재하지 않는다. *) +let main_params (m : Ast.modul) = + let rec ty_name (t : Ast.ty) = + match t with + | Ast.T_named { modl; name; _ } -> ( + match modl with Some a -> a ^ "." ^ name | None -> name) + | Ast.T_fn _ -> "" + in + List.concat_map + (function + | Ast.I_fn { decl; _ } when decl.fn_name = "main" -> + List.map + (fun (p : Ast.param) -> (p.p_name, ty_name p.p_ty)) + decl.fn_params + | _ -> []) + m.items + +let run st path : (string, error) result = + load st path; + let errs = errors st in + if errs <> [] then Error (List.hd errs) + else + match find st path with + | None -> + Error { file = path; line = 0; col = 0; message = "모듈을 찾을 수 없습니다" } + | Some e -> ( + let dep_surface = + List.concat_map + (fun (alias, p) -> + match Hashtbl.find_opt st.modules p with + | Some d -> Iface.qualify alias d.iface.Iface.items + | None -> []) + e.imports + in + let prog = Ir.of_module ~imports:dep_surface e.ast in + match Interp.run prog (main_params e.ast) with + | Ok out -> Ok out + | Error (pos, msg) -> Error (err_of path pos msg)) diff --git a/samples/README.md b/samples/README.md index 5c8f15d..9e0b6c7 100644 --- a/samples/README.md +++ b/samples/README.md @@ -33,6 +33,15 @@ capability라 자원 타입을 정의해야 affine임이 유도된다. 01~04, 06, 07은 `coolc check`를 통과한다 (exit 0). +`modules/`는 모듈 경계다. `coolc check modules/area.cool`이 import를 따라 +shapes를 먼저 검사하고, `coolc iface modules/shapes.cool`이 downstream이 +보는 표면과 그 해시를 보여준다. + +`run/`은 실제로 돈다. `coolc run run/hello.cool`. 이 파일들은 검사를 +통과한다가 아니라 무엇을 출력하는지까지 말한다 — 기대 출력이 주석에 있고 +같은 것을 test/가 검사한다. main이 선언한 capability만 런타임이 넘기므로, +파라미터에서 Console을 지우면 출력할 방법이 프로그램 안에 없다. + 파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다. ## 확정된 표기 diff --git a/samples/run/control.cool b/samples/run/control.cool new file mode 100644 index 0000000..615ee5f --- /dev/null +++ b/samples/run/control.cool @@ -0,0 +1,55 @@ +// 실행 의미를 한 파일에 모은 것: ?, mut, struct, match, scope. +// +// 기대 출력: +// 7 +// 6 +// err +// in scope + +pub capability Console { + fn print(s: String) effects {Console.print} +} + +pub enum E { + Bad, +} + +pub copyable struct P { + x: Int, + y: Int, +} + +pub fn half(n: Int) -> Result[Int, E] { + if n % 2 == 0 { + Ok(n / 2) + } else { + Err(Bad) + } +} + +pub fn twice(n: Int) -> Result[Int, E] { + let a = half(n)? + let b = half(a)? + Ok(a + b) +} + +pub fn show_res(r: Result[Int, E]) -> String { + match r { + Ok(v) => Int.show(v), + Err(_) => "err", + } +} + +pub fn main(c: Console, root: TaskScope) effects {Console.print} { + let mut total = 0 + let p = P { x: 3, y: 4 } + total = total + p.x + p.y + c.print(Int.show(total)) + c.print(show_res(twice(8))) + c.print(show_res(twice(7))) + scope sc = root { + sc.spawn(fn() { + c.print("in scope") + }) + } +} diff --git a/samples/run/hello.cool b/samples/run/hello.cool new file mode 100644 index 0000000..92a1ca3 --- /dev/null +++ b/samples/run/hello.cool @@ -0,0 +1,28 @@ +// 실행되는 첫 프로그램. +// +// main은 자기가 선언한 capability만 받는다. Console을 파라미터에서 지우면 +// print할 방법이 프로그램 안에 없다 — ambient authority가 없다는 것의 +// 실행 시점 의미다. + +pub capability Console { + fn print(s: String) effects {Console.print} +} + +pub enum Shape { + Circle(Int), + Square(Int), +} + +pub fn area(s: Shape) -> Int { + match s { + Circle(r) => r * r * 3, + Square(w) => w * w, + } +} + +pub fn main(c: Console) effects {Console.print} { + let shapes = [Circle(2), Square(3), Circle(1)] + List.each(shapes, fn(s) { + c.print(String.concat("area = ", Int.show(area(s)))) + }) +} diff --git a/test/test_coollang.ml b/test/test_coollang.ml index cd0efc7..7059a5b 100644 --- a/test/test_coollang.ml +++ b/test/test_coollang.ml @@ -986,3 +986,118 @@ let () = (fun (e : Session.error) -> e.file = b && String.length e.message > 0 && has_sub e.message "빠진 경우") (Session.errors st)) + +(* ------------------------------------------------------------------ *) +(* 인터프리터 *) +(* *) +(* 샘플은 이제 "검사를 통과한다"가 아니라 "이 값을 낸다"까지 말한다. *) +(* 실행 가능한 명세가 되는 지점이고, v1이 백엔드를 바꿔도 남는다. *) +(* ------------------------------------------------------------------ *) + +let run_src src = + let dir = Filename.concat (Filename.get_temp_dir_name ()) "cool_runtest" 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 () in + Session.run st f + +let console = + "pub capability Console {\n\ + \ fn print(s: String) effects {Console.print}\n\ + }\n\n" + +let outputs src expected = + match run_src (console ^ src) with + | Ok out -> out = expected + | Error e -> + Printf.printf " (실행 오류: %s)\n" (Session.string_of_error e); + false + +let () = + check "산술과 출력" + (outputs + "pub fn main(c: Console) effects {Console.print} {\n\ + \ c.print(Int.show(2 + 3 * 4))\n\ + }" + "14\n"); + check "match와 생성자" + (outputs + "pub enum S {\n\ + \ A(Int),\n\ + \ B,\n\ + }\n\n\ + pub fn f(s: S) -> Int {\n\ + \ match s {\n\ + \ A(n) => n + 1,\n\ + \ B => 0,\n\ + \ }\n\ + }\n\n\ + pub fn main(c: Console) effects {Console.print} {\n\ + \ c.print(Int.show(f(A(41))))\n\ + \ c.print(Int.show(f(B)))\n\ + }" + "42\n0\n"); + check "mut 바인딩과 대입" + (outputs + "pub fn main(c: Console) effects {Console.print} {\n\ + \ let mut n = 1\n\ + \ n = n + 10\n\ + \ c.print(Int.show(n))\n\ + }" + "11\n"); + check "?는 Err에서 즉시 반환한다" + (outputs + "pub enum E {\n\ + \ Bad,\n\ + }\n\n\ + pub fn half(n: Int) -> Result[Int, E] {\n\ + \ if n % 2 == 0 { Ok(n / 2) } else { Err(Bad) }\n\ + }\n\n\ + pub fn twice(n: Int) -> Result[Int, E] {\n\ + \ let a = half(n)?\n\ + \ let b = half(a)?\n\ + \ Ok(a + b)\n\ + }\n\n\ + pub fn show_res(r: Result[Int, E]) -> String {\n\ + \ match r {\n\ + \ Ok(v) => Int.show(v),\n\ + \ Err(_) => \"err\",\n\ + \ }\n\ + }\n\n\ + pub fn main(c: Console) effects {Console.print} {\n\ + \ c.print(show_res(twice(8)))\n\ + \ c.print(show_res(twice(7)))\n\ + }" + "6\nerr\n"); + check "클로저가 바깥 capability를 잡는다" + (outputs + "pub fn main(c: Console) effects {Console.print} {\n\ + \ List.each([1, 2], fn(n) {\n\ + \ c.print(Int.show(n))\n\ + \ })\n\ + }" + "1\n2\n"); + check "scope 블록은 순차로 돌고 나갈 때 join한다" + (outputs + "pub fn main(c: Console, root: TaskScope) effects {Console.print} {\n\ + \ scope sc = root {\n\ + \ sc.spawn(fn() { c.print(\"a\") })\n\ + \ sc.spawn(fn() { c.print(\"b\") })\n\ + \ }\n\ + \ c.print(\"after\")\n\ + }" + "a\nb\nafter\n"); + (* 권한은 런타임에서만 온다. main이 선언하지 않으면 존재하지 않는다. *) + check "선언하지 않은 capability는 실행 시점에도 없다" + (match run_src "pub fn main() { }" with Ok "" -> true | _ -> false); + check "런타임이 모르는 capability는 거절한다" + (match + run_src + "pub capability Db {\n\ + \ fn read() effects {Db.read} -> Int\n\ + }\n\n\ + pub fn main(d: Db) effects {Db.read} { }" + with + | Error e -> has_sub e.message "제공하지 않습니다" + | Ok _ -> false)