interp: 얇은 typed IR과 트리 워킹 인터프리터 — coolc run

문서에만 있던 실행 의미가 코드가 된다.

- 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
2026-08-30 14:58:05 +09:00
co-authored by Claude Opus 5
parent 120ff361cf
commit 1c4f46e5e5
10 changed files with 913 additions and 6 deletions
+9 -1
View File
@@ -76,7 +76,15 @@ let () =
match List.tl argv with match List.tl argv with
| "check" :: files -> check_graph files | "check" :: files -> check_graph files
| [ "iface"; file ] -> dump_iface file | [ "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 | [ "tokens"; file ] -> dump_tokens file
| [ "ast"; file ] -> dump_ast file | [ "ast"; file ] -> dump_ast file
| [ "deps"; file ] -> dump_deps file | [ "deps"; file ] -> dump_deps file
+6 -1
View File
@@ -290,8 +290,13 @@ Generics (철학 2에서 파생):
- effect 변수 (effect 다형성) - effect 변수 (effect 다형성)
- interface artifact + hash 기반 incremental invalidation - interface artifact + hash 기반 incremental invalidation
- coolc check - coolc check
- 얇은 typed IR + tree-walking interpreter (coolc run 대용) - 얇은 typed IR + tree-walking interpreter (coolc run)
※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게. ※ IR을 미루면 non-IR 전제가 스며들어 재작성 됨. 지금, 얇게.
※ 실행 시점에도 권한의 출처는 런타임 하나다. main이 선언한 capability만
넘어가고, 소스에는 capability를 만드는 문법이 없다 — 보안 정리 (i)의
실행 시점 대응물. TaskScope의 뿌리도 같은 이유로 런타임이 준다.
※ v0의 scope 실행 의미는 순차다. 구조가 먼저고 병렬성은 그 위의 최적화다 —
순서가 반대면 취소와 전파를 나중에 끼워 넣게 된다.
※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상 ※ effect check가 fast path 예산 안에 드는지가 사활 → 최우선 검증 대상
제외 (아키텍처 검증 후 얹어도 되는 것): 제외 (아키텍처 검증 후 얹어도 되는 것):
+3 -4
View File
@@ -2,7 +2,9 @@
parse -> name resolution -> type check -> effect/capability check parse -> name resolution -> type check -> effect/capability check
-> interface artifact + hash -> (coolc run 시) 얇은 typed IR -> interpreter -> 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 } 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 |> List.concat
in in
if errors <> [] then Error errors else Ok () if errors <> [] then Error errors else Ok ()
let run (file : string) : (unit, error list) result =
Error [ { file; line = 0; col = 0; message = "interpreter가 아직 구현되지 않았습니다" } ]
+340
View File
@@ -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 _ -> "<fn>"
| VCap (n, _) -> "<capability " ^ n ^ ">"
| VScope n -> "<scope " ^ 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)))
+304
View File
@@ -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
+44
View File
@@ -179,3 +179,47 @@ let errors st =
compare (a.file, a.line, a.col) (b.file, b.line, b.col)) compare (a.file, a.line, a.col) (b.file, b.line, b.col))
let find st path = Hashtbl.find_opt st.modules path 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 _ -> "<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))
+9
View File
@@ -33,6 +33,15 @@ capability라 자원 타입을 정의해야 affine임이 유도된다.
01~04, 06, 07은 `coolc check`를 통과한다 (exit 0). 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을 지우면 출력할 방법이 프로그램 안에 없다.
파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다. 파서는 첫 오류에서 멈춘다(오류 복구 미구현). 타입 검사기는 오류를 전부 모은다.
## 확정된 표기 ## 확정된 표기
+55
View File
@@ -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")
})
}
}
+28
View File
@@ -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))))
})
}
+115
View File
@@ -986,3 +986,118 @@ let () =
(fun (e : Session.error) -> (fun (e : Session.error) ->
e.file = b && String.length e.message > 0 && has_sub e.message "빠진 경우") e.file = b && String.length e.message > 0 && has_sub e.message "빠진 경우")
(Session.errors st)) (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)