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
+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