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
This commit is contained in:
2026-08-30 17:51:56 +09:00
co-authored by Claude Opus 5
parent 8ff35c5d9b
commit 78ef07d2ee
22 changed files with 551 additions and 80 deletions
+13
View File
@@ -74,6 +74,8 @@ type expr =
| E_inst of { callee : expr; args : targ list; pos : pos }
| E_try of { inner : expr; pos : pos }
| E_unary of { op : unop; operand : expr; pos : pos }
(* 복구 불가능한 실패. 타입은 Never — 돌아오지 않으므로 어떤 자리에도 놓인다 *)
| E_panic of { msg : expr; pos : pos }
| E_binary of { op : binop; lhs : expr; rhs : expr; pos : pos }
and closure = {
@@ -148,6 +150,9 @@ type item =
pos : pos;
}
| I_const of { pub : bool; name : string; ty : ty; value : expr; pos : pos }
(* 테스트. 파라미터가 없으므로 capability를 받을 수 없고, 그래서
effect-free임이 증명된다 — 관례가 아니라 검사다 *)
| I_test of { name : string; body : block; pos : pos }
type modul = { items : item list }
@@ -244,6 +249,10 @@ let binop_name = function
let rec buf_expr b = function
| E_lit (l, _) -> buf_lit b l
| E_panic { msg; _ } ->
Buffer.add_string b "(panic ";
buf_expr b msg;
Buffer.add_char b ')'
| E_ident (n, _) -> Buffer.add_string b n
| E_list (xs, _) ->
Buffer.add_string b "(list";
@@ -468,6 +477,10 @@ let buf_item b = function
Buffer.add_char b ' ';
buf_expr b value;
Buffer.add_char b ')'
| I_test { name; body; _ } ->
Buffer.add_string b ("(test \"" ^ name ^ "\" ");
buf_block b body;
Buffer.add_char b ')'
let show_item item =
let b = Buffer.create 256 in
+48 -30
View File
@@ -88,7 +88,8 @@ let tokenize (src : string) : (tok * int) array =
let start = !i + 1 in
incr i;
while !i < n && src.[!i] <> quote do
if src.[!i] = '\n' then raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
if src.[!i] = '\n' then
raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
incr i
done;
if !i >= n then raise (Error { line = !line; msg = "단말이 닫히지 않았습니다" });
@@ -115,7 +116,7 @@ let tokenize (src : string) : (tok * int) array =
emit t;
incr i
in
(match c with
match c with
| '=' -> single T_eq
| ';' -> single T_semi
| ',' -> single T_comma
@@ -130,9 +131,7 @@ let tokenize (src : string) : (tok * int) array =
| '<' -> single T_lt
| '>' -> single T_gt
| _ ->
raise
(Error
{ line = !line; msg = Printf.sprintf "알 수 없는 문자 %c" c }))
raise (Error { line = !line; msg = Printf.sprintf "알 수 없는 문자 %c" c })
end
done;
emit T_eof;
@@ -147,12 +146,10 @@ type state = { toks : (tok * int) array; mutable p : int }
let cur st = fst st.toks.(st.p)
let line st = snd st.toks.(st.p)
let adv st = if st.p < Array.length st.toks - 1 then st.p <- st.p + 1
let fail st msg = raise (Error { line = line st; msg })
let eat st t what =
if cur st = t then adv st
else fail st (Printf.sprintf "%s이(가) 필요합니다" what)
if cur st = t then adv st else fail st (Printf.sprintf "%s이(가) 필요합니다" what)
(* alt := seq { "|" seq } *)
let rec parse_alt st =
@@ -265,7 +262,8 @@ let parse (src : string) : t =
done;
List.rev !rules
let parse_result src = match parse src with r -> Ok r | exception Error e -> Error e
let parse_result src =
match parse src with r -> Ok r | exception Error e -> Error e
(* ------------------------------------------------------------------ *)
(* 조회 *)
@@ -280,7 +278,8 @@ let undefined (g : t) : string list =
let seen = Hashtbl.create 32 in
let rec walk = function
| Ref n -> if not (List.mem n defined) then Hashtbl.replace seen n ()
| RefArg (n, _) -> if not (List.mem n defined) then Hashtbl.replace seen n ()
| RefArg (n, _) ->
if not (List.mem n defined) then Hashtbl.replace seen n ()
| Term _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
@@ -309,7 +308,8 @@ let unreachable (g : t) ~(start : string) : string list =
in
List.iter (fun r -> walk r.body) g;
List.filter_map
(fun r -> if r.name = start || Hashtbl.mem used r.name then None else Some r.name)
(fun r ->
if r.name = start || Hashtbl.mem used r.name then None else Some r.name)
g
let rec show_expr = function
@@ -326,7 +326,9 @@ and show_paren e =
match e with Alt _ | Seq _ -> "( " ^ show_expr e ^ " )" | _ -> show_expr e
let show_rule r =
let ps = if r.params = [] then "" else "<" ^ String.concat ", " r.params ^ ">" in
let ps =
if r.params = [] then "" else "<" ^ String.concat ", " r.params ^ ">"
in
r.name ^ ps ^ " = " ^ show_expr r.body ^ " ;"
(* ------------------------------------------------------------------ *)
@@ -356,13 +358,13 @@ let expand (g : t) : t =
| Term _ -> e
(* 매개변수 이름이 그대로 참조된 자리도 인자로 바꾼다: list<item>의 item *)
| Ref n -> ( match List.assoc_opt n env with Some v -> Ref v | None -> e)
| RefArg (n, a) ->
| RefArg (n, a) -> (
let a = match List.assoc_opt a env with Some v -> v | None -> a in
(match by_name_arg n a with
match by_name_arg n a with
| Some r when r.params <> [] ->
let key = (n, a) in
if not (Hashtbl.mem out (mangle n a)) && not (List.mem key !queue) then
queue := key :: !queue;
if (not (Hashtbl.mem out (mangle n a))) && not (List.mem key !queue)
then queue := key :: !queue;
Ref (mangle n a)
| _ -> Ref n)
| Seq xs -> Seq (List.map (subst env) xs)
@@ -398,8 +400,9 @@ let expand (g : t) : t =
else
Hashtbl.fold
(fun k v acc ->
if String.length k > String.length r.name
&& String.sub k 0 (String.length r.name + 1) = r.name ^ "<"
if
String.length k > String.length r.name
&& String.sub k 0 (String.length r.name + 1) = r.name ^ "<"
then v :: acc
else acc)
out []
@@ -429,7 +432,8 @@ let is_token a n = SS.mem n a.tokens || find a.rules n = None
let rec nullable_expr a = function
| Term _ -> false
| Ref n -> if is_token a n then false else Hashtbl.find_opt a.nullable n = Some true
| Ref n ->
if is_token a n then false else Hashtbl.find_opt a.nullable n = Some true
| RefArg (n, x) -> nullable_expr a (Ref (mangle n x))
| Seq xs -> List.for_all (nullable_expr a) xs
| Alt xs -> List.exists (nullable_expr a) xs
@@ -438,11 +442,13 @@ let rec nullable_expr a = function
let rec first_expr a = function
| Term s -> SS.singleton s
| Ref n ->
| Ref n -> (
if is_token a n then SS.singleton n
else ( match Hashtbl.find_opt a.first n with Some s -> s | None -> SS.empty)
else
match Hashtbl.find_opt a.first n with Some s -> s | None -> SS.empty)
| RefArg (n, x) -> first_expr a (Ref (mangle n x))
| Alt xs -> List.fold_left (fun acc x -> SS.union acc (first_expr a x)) SS.empty xs
| Alt xs ->
List.fold_left (fun acc x -> SS.union acc (first_expr a x)) SS.empty xs
| Opt x | Rep x -> first_expr a x
| Except (x, _) -> first_expr a x
| Seq xs ->
@@ -476,7 +482,11 @@ let analyze ?(tokens = []) (g : t) : analysis =
Hashtbl.replace a.nullable r.name true;
changed := true);
let f = first_expr a r.body in
let old = match Hashtbl.find_opt a.first r.name with Some s -> s | None -> SS.empty in
let old =
match Hashtbl.find_opt a.first r.name with
| Some s -> s
| None -> SS.empty
in
if not (SS.equal f old) then (
Hashtbl.replace a.first r.name (SS.union old f);
changed := true))
@@ -498,10 +508,12 @@ let nullable a name = Hashtbl.find_opt a.nullable name = Some true
type follow_env = {
a : analysis;
fol : (string, SS.t) Hashtbl.t;
mutable deps : (string * string) list; (* (n, owner): follow n ⊇ follow owner *)
mutable deps : (string * string) list;
(* (n, owner): follow n ⊇ follow owner *)
}
let get_fol e n = match Hashtbl.find_opt e.fol n with Some s -> s | None -> SS.empty
let get_fol e n =
match Hashtbl.find_opt e.fol n with Some s -> s | None -> SS.empty
let rec collect e owner expr (cont : SS.t) (cont_end : bool) =
match expr with
@@ -576,13 +588,18 @@ let conflicts ?(tokens = []) ?(greedy = []) (g : t) : conflict list =
c_tokens = toks;
c_detail = detail;
(* 선택/반복만 greedy로 해소된다. 대안(Alt) 충돌은 못 덮는다 *)
c_greedy = kind <> "선택" && List.for_all (fun t -> List.mem t greedy) toks;
c_greedy =
kind <> "선택" && List.for_all (fun t -> List.mem t greedy) toks;
}
:: !out
in
let rec walk r expr (cont : SS.t) (cont_end : bool) =
let cont_full =
if cont_end then SS.union cont (match Hashtbl.find_opt fol r.name with Some s -> s | None -> SS.empty)
if cont_end then
SS.union cont
(match Hashtbl.find_opt fol r.name with
| Some s -> s
| None -> SS.empty)
else cont
in
match expr with
@@ -592,12 +609,13 @@ let conflicts ?(tokens = []) ?(greedy = []) (g : t) : conflict list =
let n = List.length xs in
for i = 0 to n - 1 do
for j = i + 1 to n - 1 do
let fi = first_expr a (List.nth xs i) and fj = first_expr a (List.nth xs j) in
let fi = first_expr a (List.nth xs i)
and fj = first_expr a (List.nth xs j) in
let inter = SS.inter fi fj in
if not (SS.is_empty inter) then
add r "선택" (SS.elements inter)
(Printf.sprintf "%d번째와 %d번째 대안이 같은 토큰으로 시작합니다: %s / %s"
(i + 1) (j + 1)
(Printf.sprintf "%d번째와 %d번째 대안이 같은 토큰으로 시작합니다: %s / %s" (i + 1)
(j + 1)
(show_expr (List.nth xs i))
(show_expr (List.nth xs j)))
done
+13 -7
View File
@@ -16,7 +16,9 @@ let min_len (g : Ebnf.t) (tokens : string list) : (string, int) Hashtbl.t =
let inf = 1_000_000 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace tbl r.name inf) g;
let get n =
if List.mem n tokens || not (List.exists (fun (r : Ebnf.rule) -> r.name = n) g)
if
List.mem n tokens
|| not (List.exists (fun (r : Ebnf.rule) -> r.name = n) g)
then 1
else match Hashtbl.find_opt tbl n with Some v -> v | None -> 1
in
@@ -88,10 +90,12 @@ let deep g = g.depth >= g.max_depth
let rec gen_expr g (e : Ebnf.expr) =
match e with
| Ebnf.Term s -> (
match token_of_term s with Some k -> emit g k | None -> emit g (Token.Ident "x"))
| Ebnf.Ref n ->
match token_of_term s with
| Some k -> emit g k
| None -> emit g (Token.Ident "x"))
| Ebnf.Ref n -> (
if List.mem n g.tokens then emit g (sample_token n)
else (
else
match Hashtbl.find_opt g.rules n with
| Some r ->
Hashtbl.replace g.visited n ();
@@ -106,7 +110,9 @@ let rec gen_expr g (e : Ebnf.expr) =
if deep g then begin
(* 깊이가 차면 가장 짧게 끝나는 가지. 같은 값이 여럿이면 무작위로
고른다 — 늘 첫 번째를 고르면 뒤쪽 가지가 영영 안 밟힌다 *)
let best = List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs in
let best =
List.fold_left (fun a x -> min a (cost_of g x)) 1_000_000 xs
in
let cands = List.filter (fun x -> cost_of g x = best) xs in
match cands with
| [] -> None
@@ -144,5 +150,5 @@ let sentence ?(tokens = []) ?(start = "module") ?(max_depth = 14) ?visited
in
gen_expr st (Ebnf.Ref start);
let pos = Token.{ line = 1; col = 1 } in
List.rev_map (fun k -> Token.{ kind = k; pos }) st.out
|> fun xs -> List.rev (Token.{ kind = Token.Eof; pos } :: List.rev xs)
List.rev_map (fun k -> Token.{ kind = k; pos }) st.out |> fun xs ->
List.rev (Token.{ kind = Token.Eof; pos } :: List.rev xs)
+4
View File
@@ -25,6 +25,9 @@ let is_exported = function
| I_capability { pub; _ } -> pub
| I_const { pub; _ } -> pub
| I_reexport _ -> true
(* 테스트는 표면이 아니다. 테스트를 고쳤다고 downstream이 재검사되면
안 된다 — 함수 본문과 같은 이유다 *)
| I_test _ -> false
| I_import _ -> false
let strip = function
@@ -38,6 +41,7 @@ let item_name = function
| I_capability { name; _ } -> name
| I_const { name; _ } -> name
| I_reexport { name; _ } -> name
| I_test { name; _ } -> name
| I_import { alias; _ } -> alias
(* reexport는 이름이 아니라 해소된 정의 본문이 hash에 들어간다.
+38 -2
View File
@@ -320,6 +320,8 @@ let rec eval st (env : env) (e : Ir.t) : value =
| [ f ] -> apply st pos f []
| _ -> fail pos "spawn은 함수 하나를 받습니다")
| other -> fail pos (Printf.sprintf "%s에는 필드가 없습니다" (show other)))
| Ir.I_panic (msg, pos) -> (
match eval st env msg with VStr s -> fail pos s | v -> fail pos (show v))
| Ir.I_unary (op, e, pos) -> (
match (op, eval st env e) with
| Ast.U_not, VBool b -> VBool (not b)
@@ -488,8 +490,8 @@ and eval_binary st env op a b pos =
print는 실제로 일어난 effect다. 일어난 일을 안 보여주면 "어디까지 갔나"를
알 수 없고, 그게 실패했을 때 가장 먼저 보고 싶은 것이다. *)
let run ?(args = []) (prog : Ir.program) (entry : string)
(main_params : (string * string) list) : string * (Token.pos * string) option
=
(main_params : (string * string) list) :
string * (Token.pos * string) option =
Buffer.clear out;
argv := args;
let st = { prog } in
@@ -515,3 +517,37 @@ let run ?(args = []) (prog : Ir.program) (entry : string)
with
| Fail (pos, msg) -> (Buffer.contents out, Some (pos, msg))
| Return_exc _ -> (Buffer.contents out, None)))
(* ------------------------------------------------------------------ *)
(* 테스트 러너 *)
(* *)
(* 격리는 런타임의 일이지 언어의 일이 아니다. 언어에는 recover가 없고, *)
(* 러너는 죽은 것을 되살리는 것이 아니라 죽었다는 사실을 관찰하고 다음 *)
(* 으로 간다 — 프로세스 경계에 가깝다. *)
(* *)
(* 테스트가 effect-free임은 검사기가 이미 보장한다. 그래서 순서에 *)
(* 의존하지 않고, 어떤 순서로 돌려도 같다. *)
(* ------------------------------------------------------------------ *)
type test_result = {
t_module : string;
t_name : string;
t_pos : Token.pos;
t_failure : (Token.pos * string) option;
}
let run_tests ?(filter = "") (prog : Ir.program) : test_result list =
let st = { prog } in
List.rev prog.Ir.tests
|> List.filter (fun (_, name, _, _) ->
filter = "" || find_sub name filter <> None)
|> List.map (fun (m, name, pos, body) ->
let failure =
try
ignore (eval st [ [] ] body);
None
with
| Fail (p, msg) -> Some (p, msg)
| Return_exc _ -> None
in
{ t_module = m; t_name = name; t_pos = pos; t_failure = failure })
+11
View File
@@ -45,6 +45,7 @@ type t =
| 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_panic of t * pos
| I_unary of Ast.unop * t * pos
| I_binary of Ast.binop * t * t * pos
@@ -60,6 +61,8 @@ type fn = { fn_name : string; fn_params : string list; fn_body : t }
type program = {
fns : (string, fn) Hashtbl.t;
(* 테스트. (모듈 경로, 이름, 위치, 본문) *)
mutable tests : (string * string * pos * t) list;
(* variant 이름 -> (enum 이름, 인자 개수) *)
ctors : (string, string * int) Hashtbl.t;
caps : (string, string list) Hashtbl.t; (* capability -> 메서드 이름 *)
@@ -183,6 +186,7 @@ let rec lower_pat c (p : Ast.pattern) : pat =
let rec lower c (e : Ast.expr) : t =
match e with
| Ast.E_panic { msg; pos } -> I_panic (lower c msg, pos)
| 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)
@@ -310,6 +314,7 @@ let of_program (mods : modinfo list) : program =
let prog =
{
fns = Hashtbl.create 64;
tests = [];
ctors = Hashtbl.create 64;
caps = Hashtbl.create 16;
consts = Hashtbl.create 16;
@@ -377,6 +382,12 @@ let of_program (mods : modinfo list) : program =
| Ast.I_const { name; value; _ } ->
c.locals <- [];
Hashtbl.replace prog.consts (key name) (lower c value)
| Ast.I_test { name; body; pos } ->
c.locals <- [];
lpush c;
let b = lower_block c body in
lpop c;
prog.tests <- (mi.m_path, name, pos, b) :: prog.tests
| _ -> ())
mi.m_ast.items)
mods;
+3 -1
View File
@@ -19,7 +19,8 @@ let is_keyword (k : Token.kind) = Token.keyword (Token.show_kind k) = Some k
let keywords () =
List.filter_map
(fun k -> if is_keyword k then Some ("\"" ^ Token.show_kind k ^ "\"") else None)
(fun k ->
if is_keyword k then Some ("\"" ^ Token.show_kind k ^ "\"") else None)
Token.all_kinds
let statement_enders () =
@@ -28,6 +29,7 @@ let statement_enders () =
Token.all_kinds
let begin_mark = "(* 여기부터 lib/token.ml에서 생성됩니다 — 손으로 고치지 마십시오 *)"
let end_mark = "(* 생성 끝 *)"
(* 한 줄이 길어지지 않게 접는다. 구분자는 줄 끝에 남겨 이어짐이 보이게 한다 *)
+14 -1
View File
@@ -187,6 +187,10 @@ 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
@@ -463,7 +467,16 @@ let check ?(imports : item list = []) (m : modul) : error list =
| _ -> ())
(imports @ m.items);
List.iter
(fun it -> match it with I_fn { decl; _ } -> check_fn st decl | _ -> ())
(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 ->
+16
View File
@@ -398,6 +398,12 @@ and parse_primary st =
| Token.Kw_fn -> parse_closure st
| Token.Kw_if -> parse_if st
| Token.Kw_match -> parse_match st
| Token.Kw_panic ->
adv st;
expect st Token.LParen "(";
let msg = with_struct_ok st (fun () -> parse_expr st) in
expect_close st Token.RParen ")";
E_panic { msg; pos = p }
| Token.Kw_scope ->
adv st;
let name = ident st "새 scope 이름" in
@@ -759,6 +765,16 @@ let parse_item st =
adv st;
let name = ident st "재수출할 이름" in
I_reexport { name; pos = p }
| Token.Kw_test ->
adv st;
let name =
match kind st with
| Token.Str s ->
adv st;
s
| _ -> err_expect st "테스트 이름 (문자열)"
in
I_test { name; body = parse_block st; pos = p }
| _ -> (
let pub = accept st Token.Kw_pub in
match kind st with
+7 -7
View File
@@ -48,11 +48,11 @@ let fail st expected =
(e.pos.Token.line, e.pos.Token.col)
<= ((cur st).Token.pos.Token.line, (cur st).Token.pos.Token.col)
in
if keep then st.err <- Some { pos = (cur st).Token.pos; expected; got = term st };
if keep then
st.err <- Some { pos = (cur st).Token.pos; expected; got = term st };
raise Fail
let advance st = if st.i < Array.length st.toks - 1 then st.i <- st.i + 1
let is_tok st n = SS.mem n st.tokens || not (Hashtbl.mem st.rules n)
(* 이 식이 지금 토큰으로 시작할 수 있는가 *)
@@ -75,8 +75,7 @@ let rec run st (e : Ebnf.expr) =
| Some x -> run st x
| None ->
fail st
(String.concat " 또는 "
(SS.elements (Ebnf.first_expr st.a e)))))
(String.concat " 또는 " (SS.elements (Ebnf.first_expr st.a e)))))
(* 선택과 반복은 최대한 먹는다 (문법 표기 규약의 greedy 규칙) *)
| Ebnf.Opt x -> if starts st x then run st x
| Ebnf.Rep x ->
@@ -85,8 +84,8 @@ let rec run st (e : Ebnf.expr) =
done
| Ebnf.Except (x, _) -> run st x
let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list) :
(unit, error) result =
let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list)
: (unit, error) result =
let g = Ebnf.expand g in
let rules = Hashtbl.create 128 in
List.iter (fun (r : Ebnf.rule) -> Hashtbl.replace rules r.name r) g;
@@ -108,6 +107,7 @@ let check ?(tokens = []) ?(start = "module") (g : Ebnf.t) (toks : Token.t list)
| Some _ -> ()
| None ->
st.err <-
Some { pos = (cur st).Token.pos; expected = "파일 끝"; got = term st });
Some
{ pos = (cur st).Token.pos; expected = "파일 끝"; got = term st });
Error (Option.get st.err))
| exception Fail -> Error (Option.get st.err)
+8 -3
View File
@@ -154,6 +154,7 @@ let rec resolve_pattern st seen = function
let rec resolve_expr st = function
| E_lit _ -> ()
| E_panic { msg; _ } -> resolve_expr st msg
| E_ident (n, pos) ->
if lookup_local st n = None then
if Hashtbl.mem st.items n then ()
@@ -261,8 +262,7 @@ and resolve_stmt st = function
| _ -> None
in
match root place with
| None ->
error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
| None -> error st pos "대입 왼쪽에는 변수나 필드만 올 수 있습니다"
| Some n -> (
match lookup_local st n with
| Some true -> ()
@@ -351,7 +351,7 @@ let resolve (m : modul) : info * error list =
variants
| I_capability { name; pos; _ } -> declare st pos name K_type
| I_const { name; pos; _ } -> declare st pos name K_const
| I_reexport _ -> ())
| I_test _ | I_reexport _ -> ())
m.items;
(* 2차: 본문을 훑는다. *)
List.iter
@@ -379,6 +379,11 @@ let resolve (m : modul) : info * error list =
resolve_ty st ty;
push st;
resolve_expr st value;
pop st
(* 테스트는 파라미터 없는 본문이다. 나머지는 함수와 같다 *)
| I_test { body; _ } ->
push st;
resolve_block st body;
pop st)
m.items;
(* 미사용 import는 오류다. 취향 문제가 아니라 invalidation 표면 문제다 —
+14
View File
@@ -279,3 +279,17 @@ let run ?(args = []) st path : string * error option =
match Interp.run ~args prog path (main_params e.ast) with
| out, None -> (out, None)
| out, Some (pos, msg) -> (out, Some (err_of path pos msg)))
(* 모듈 그래프를 로드하고 그 안의 테스트를 전부 돌린다 *)
let test ?(filter = "") st path : (Interp.test_result list, error list) result =
load st path;
let errs = errors st in
if errs <> [] then Error errs
else
let mods =
Hashtbl.fold
(fun p (d : entry) acc ->
{ Ir.m_path = p; m_ast = d.ast; m_deps = d.imports } :: acc)
st.modules []
in
Ok (Interp.run_tests ~filter (Ir.of_program mods))
+12 -2
View File
@@ -28,6 +28,8 @@ type kind =
| Kw_else
| Kw_match
| Kw_scope
| Kw_panic
| Kw_test
| Kw_true
| Kw_false
(* 구두점 *)
@@ -87,6 +89,8 @@ let keyword = function
| "else" -> Some Kw_else
| "match" -> Some Kw_match
| "scope" -> Some Kw_scope
| "panic" -> Some Kw_panic
| "test" -> Some Kw_test
| "true" -> Some Kw_true
| "false" -> Some Kw_false
| _ -> None
@@ -115,6 +119,8 @@ let show_kind = function
| Kw_else -> "else"
| Kw_match -> "match"
| Kw_scope -> "scope"
| Kw_panic -> "panic"
| Kw_test -> "test"
| Kw_true -> "true"
| Kw_false -> "false"
| LParen -> "("
@@ -178,7 +184,9 @@ let next_kind = function
| Kw_if -> Some Kw_else
| Kw_else -> Some Kw_match
| Kw_match -> Some Kw_scope
| Kw_scope -> Some Kw_true
| Kw_scope -> Some Kw_panic
| Kw_panic -> Some Kw_test
| Kw_test -> Some Kw_true
| Kw_true -> Some Kw_false
| Kw_false -> Some LParen
| LParen -> Some RParen
@@ -215,7 +223,9 @@ let next_kind = function
let all_kinds =
let rec go k acc =
match next_kind k with None -> List.rev (k :: acc) | Some n -> go n (k :: acc)
match next_kind k with
| None -> List.rev (k :: acc)
| Some n -> go n (k :: acc)
in
go (Ident "") []
+27
View File
@@ -164,6 +164,12 @@ let builtin_ctor = function
let rec infer env (e : expr) : T.t =
match e with
(* panic은 돌아오지 않는다. 타입은 Never이고 어떤 자리에도 놓인다 *)
| E_panic { msg; pos } ->
let t = infer env msg in
if not (T.unify t T.TString) then
mismatch env pos T.TString t "panic의 메시지";
T.TNever
| E_lit (L_int _, _) -> T.TInt
| E_lit (L_str _, _) -> T.TString
| E_lit (L_bool _, _) -> T.TBool
@@ -823,6 +829,27 @@ let check ?(imports : item list = []) (m : modul) : error list =
let want = conv env [] ty in
let got = infer env value in
if not (T.unify want got) then mismatch env pos want got "상수의 값"
(* 테스트는 파라미터 없고 effect 없는 함수와 같다. 검사도 같다 —
일반 코드와 다른 규칙을 주면 테스트만 통과하는 코드가 생긴다 *)
| I_test { name; body; pos } ->
env.locals <- [];
push env;
env.ret <- T.TUnit;
env.performed <- [];
env.saw_unknown <- false;
let got = infer_block env body in
if not (T.unify T.TUnit got) then
mismatch env pos T.TUnit got (Printf.sprintf "테스트 \"%s\"의 본문" name);
List.iter
(fun (a, apos) ->
err env apos
(Printf.sprintf
"테스트는 effect를 수행할 수 없습니다 (%s) — 테스트는 capability를 받지 않습니다"
(T.atom_show a)))
(List.rev (T.eff_resolve (List.map fst env.performed))
|> List.map (fun a -> (a, pos)));
env.performed <- [];
pop env
| _ -> ())
m.items;
List.sort
+7
View File
@@ -10,6 +10,10 @@
type t =
| TUnknown
(* 값을 내지 않는 타입. panic의 타입이고 어떤 자리에도 놓일 수 있다.
TUnknown과 다르다 — TUnknown은 "모른다"이고 TNever는 "돌아오지
않는다"이다. 둘 다 무엇과도 맞지만 생기는 이유가 다르다. *)
| TNever
| TInt
| TBool
| TString
@@ -97,6 +101,7 @@ let eff_missing ~declared ~performed =
let rec show t =
match resolve t with
| TUnknown -> "?"
| TNever -> "Never"
| TInt -> "Int"
| TBool -> "Bool"
| TString -> "String"
@@ -136,6 +141,8 @@ let unify_eff a b =
let rec unify a b =
match (resolve a, resolve b) with
| TUnknown, _ | _, TUnknown -> true
(* Never는 어떤 타입 자리에도 놓인다. panic이 match 팔에 설 수 있는 이유다 *)
| TNever, _ | _, TNever -> true
| TMeta r, TMeta r' when r == r' -> true
| TMeta r, t | t, TMeta r ->
if occurs r t then false