Files
coollang/test/test_coollang.ml
T
coolguyandClaude Opus 5 1bd88fc4d7 parser: 재귀 하강 구문 분석기와 AST
grammar.ebnf의 프로덕션 하나에 함수 하나로 대응한다. LL(1)이므로 선읽기는
항상 한 토큰이고 backtracking이 없다. 모든 노드가 위치를 들고 다닌다.

문법과 얽히는 두 자리를 원칙대로 처리했다:
- NEWLINE 흡수는 문법에 { NEWLINE }으로 적힌 자리에서만 한다. 파서가
  "여기선 줄바꿈 무시" 식으로 임의 판단하면 어디서 무시되는지 아무도 모르게
  된다.
- struct 리터럴은 if/match/scope 머리에서 금지하고(no_struct), 괄호·인자
  목록·블록에 들어가면 다시 허용한다.

문법에 새긴 제한이 실제로 파서에서 죽는 것을 확인했다. 파라미터 위치의
effect 합집합과 match 가드는 검사기가 아니라 파서가 거부하며, 진단이
원인을 직접 말한다. 후행 콤마 누락도 일반적인 "닫는 괄호 필요" 대신
"다중 줄 목록에는 후행 콤마가 필요합니다"로 보고한다.

샘플을 실제로 파싱해 두 가지를 잡았다:
- 02와 05가 own을 타입 위치에 쓰고 있었다. 확정한 규칙은 바인딩 수식어가
  이름 앞이므로 샘플이 틀렸다. 수정.
- 05에 구문 오류와 검사기 오류가 섞여 있었다. 파서가 첫 오류에서 멈추면
  검사기 케이스에 영영 도달하지 못하므로 08_syntax_errors.cool로 분리.

grammar.ebnf를 구현과 맞췄다: 제네릭 인자에 effect 집합 허용, 마지막 문의
구분자는 "}" 앞에서 생략, 중괄호 목록 안의 NEWLINE 흡수 위치 명시.

cool ast 추가. cool check는 이제 파서까지 돌리되 여전히 통과했다고 말하지
않는다.

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

365 lines
11 KiB
OCaml

open Coollang
let failures = ref 0
let check name cond =
if not cond then (
incr failures;
Printf.printf "FAIL %s\n" name)
let kinds src =
match Lexer.lex_result src with
| Ok ts -> List.map (fun t -> t.Token.kind) ts
| Error e ->
failwith
(Printf.sprintf "예상치 못한 렉서 오류 %d:%d %s" e.pos.line e.pos.col e.msg)
let lex_error src =
match Lexer.lex_result src with Ok _ -> None | Error e -> Some e.msg
(* --- 키워드와 이름 --- *)
let () =
check "키워드 인식"
(kinds "fn own affine"
= [ Token.Kw_fn; Token.Kw_own; Token.Kw_affine; Token.Eof ]);
check "이름은 키워드가 아니다"
(kinds "own_er" = [ Token.Ident "own_er"; Token.Newline; Token.Eof ])
(* --- 두 글자 연산자를 먼저 본다 --- *)
let () =
check "화살표"
(kinds "-> => == != <= >= && ||"
= [
Token.Arrow;
Token.FatArrow;
Token.EqEq;
Token.BangEq;
Token.Le;
Token.Ge;
Token.AmpAmp;
Token.PipePipe;
Token.Eof;
]);
check "한 글자로 갈리는 자리"
(kinds "- = ! < > |"
= [
Token.Minus;
Token.Eq;
Token.Bang;
Token.Lt;
Token.Gt;
Token.Pipe;
Token.Eof;
])
(* --- NEWLINE 삽입 (grammar.ebnf 어휘 절) --- *)
let () =
check "값으로 끝난 줄 뒤에 삽입"
(kinds "a\nb"
= [
Token.Ident "a";
Token.Newline;
Token.Ident "b";
Token.Newline;
Token.Eof;
]);
check "여는 괄호로 끝난 줄은 이어진다"
(kinds "f(\na)"
= [
Token.Ident "f";
Token.LParen;
Token.Ident "a";
Token.RParen;
Token.Newline;
Token.Eof;
]);
check "연산자로 끝난 줄은 이어진다"
(kinds "a +\nb"
= [ Token.Ident "a"; Token.Plus; Token.Ident "b"; Token.Newline; Token.Eof ]
);
check "빈 줄은 구분자를 만들지 않는다"
(kinds "a\n\n\nb"
= [
Token.Ident "a";
Token.Newline;
Token.Ident "b";
Token.Newline;
Token.Eof;
]);
check "주석만 있는 줄도 마찬가지"
(kinds "a\n// 설명\nb"
= [
Token.Ident "a";
Token.Newline;
Token.Ident "b";
Token.Newline;
Token.Eof;
]);
check "닫는 괄호 뒤에는 삽입"
(kinds "f()\ng()"
= [
Token.Ident "f";
Token.LParen;
Token.RParen;
Token.Newline;
Token.Ident "g";
Token.LParen;
Token.RParen;
Token.Newline;
Token.Eof;
]);
check "? 뒤에는 삽입"
(kinds "f()?\ng"
= [
Token.Ident "f";
Token.LParen;
Token.RParen;
Token.Question;
Token.Newline;
Token.Ident "g";
Token.Newline;
Token.Eof;
]);
check "빈 입력" (kinds "" = [ Token.Eof ])
(* --- 리터럴 --- *)
let () =
check "정수와 밑줄"
(kinds "1_000" = [ Token.Int "1_000"; Token.Newline; Token.Eof ]);
check "문자열 이스케이프"
(kinds "\"a\\nb\"" = [ Token.Str "a\nb"; Token.Newline; Token.Eof ]);
check "밑줄 단독" (kinds "_" = [ Token.Underscore; Token.Eof ])
(* --- 오류 --- *)
let () =
check "닫히지 않은 문자열" (lex_error "\"abc" <> None);
check "줄바꿈 전에 닫지 않은 문자열" (lex_error "\"abc\ndef\"" <> None);
check "알 수 없는 이스케이프" (lex_error "\"a\\qb\"" <> None);
check "이름은 문자로 시작" (lex_error "_foo" <> None);
check "단독 &" (lex_error "a & b" <> None);
check "정수 뒤 글자" (lex_error "12abc" <> None);
check "예상치 못한 문자" (lex_error "a @ b" <> None)
(* --- 오류 위치 --- *)
let () =
match Lexer.lex_result "a\nb @ c" with
| Ok _ -> check "위치 보고" false
| Error e -> check "위치 보고" (e.pos.line = 2 && e.pos.col = 3)
(* --- 샘플 전체가 어휘 분석을 통과해야 한다 --- *)
let () =
let dir = "../samples" in
let files =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.sort compare
in
check "샘플이 존재한다" (List.length files >= 7);
List.iter
(fun f ->
let path = Filename.concat dir f in
match Driver.tokens path with
| Ok ts -> check (f ^ " 어휘 분석") (List.length ts > 10)
| Error errors ->
List.iter
(fun e -> Printf.printf " %s\n" (Driver.string_of_error e))
errors;
check (f ^ " 어휘 분석") false)
files
(* --- 기존 계약 --- *)
let () =
check "버전 비어있지 않음" (String.length Version.string > 0);
match Driver.check [] with Error [ _ ] -> () | _ -> check "빈 입력에는 오류" false
let () =
if !failures = 0 then print_endline "ok"
else (
Printf.printf "%d개 실패\n" !failures;
exit 1)
(* --- ASI 함정: effects 절이 줄 끝에 오면 } 뒤에 NEWLINE이 삽입된다.
렉서는 이대로 두고 문법이 { NEWLINE }으로 흡수한다 (grammar.ebnf). --- *)
let () =
check "닫는 중괄호 뒤 줄바꿈은 삽입된다"
(kinds "effects {A.b}\n-> Int"
= [
Token.Kw_effects;
Token.LBrace;
Token.Ident "A";
Token.Dot;
Token.Ident "b";
Token.RBrace;
Token.Newline;
Token.Arrow;
Token.Ident "Int";
Token.Newline;
Token.Eof;
]);
check "후행 콤마가 있으면 목록이 이어진다"
(kinds "f(\na,\nb,\n)"
= [
Token.Ident "f";
Token.LParen;
Token.Ident "a";
Token.Comma;
Token.Ident "b";
Token.Comma;
Token.RParen;
Token.Newline;
Token.Eof;
])
(* ================================================================== *)
(* 파서 *)
(* ================================================================== *)
let parse_ok src =
match Lexer.lex_result src with
| Error e ->
failwith (Printf.sprintf "렉서 오류 %d:%d %s" e.pos.line e.pos.col e.msg)
| Ok ts -> (
match Parser.parse_result ts with
| Ok m -> m
| Error e ->
failwith (Printf.sprintf "파서 오류 %d:%d %s" e.pos.line e.pos.col e.msg))
let parse_err src =
match Lexer.lex_result src with
| Error e -> Some e.msg
| Ok ts -> (
match Parser.parse_result ts with Ok _ -> None | Error e -> Some e.msg)
let one src =
match (parse_ok src).Ast.items with
| [ it ] -> Ast.show_item it
| items -> Printf.sprintf "<항목 %d개>" (List.length items)
(* --- 어순: effects 절은 파라미터 뒤, 화살표 앞 --- *)
let () =
check "함수 선언 어순"
(one "pub fn f(a: Int) effects {A.b} -> Int {\n a\n}"
= "pub (fn f (a:Int) [eset A.b] -> Int (block a))");
check "결과 위치의 effect 합집합"
(one "fn f() effects e1 | e2" = "(fn f () [evar e1 | evar e2] decl)");
check "함수 타입의 어순과 중첩"
(one "fn f(g: fn(Int) effects e -> Bool) -> fn(Int) -> Int"
= "(fn f (g:(fn (Int) [evar e] -> Bool)) -> (fn (Int) -> Int) decl)")
(* --- 대괄호 제네릭: 전위는 리터럴, 후위는 인스턴스화 --- *)
let () =
check "타입 위치의 대괄호"
(one "fn f(xs: List[Int]) -> Result[a, Error]"
= "(fn f (xs:(List Int)) -> (Result a Error) decl)");
check "식 위치의 후위 대괄호는 인스턴스화"
(one "fn f() {\n map[Path, Int, {A.b}](xs, g)\n}"
= "(fn f () (block (call (inst map Path Int [eset A.b]) xs g)))");
check "전위 대괄호는 리스트 리터럴"
(one "fn f() {\n let xs = [1, 2, 3]\n xs\n}"
= "(fn f () (block (let xs (list 1 2 3)) xs))")
(* --- 파라미터 수식어 순서 --- *)
let () =
check "own은 이름 앞, affine은 타입 안"
(one "fn f(own a: affine fn() effects {F.c}, mut b: Int)"
= "(fn f (own a:(affine-fn () [eset F.c]) mut b:Int) decl)")
(* --- 선언 --- *)
let () =
check "copyable struct"
(one "pub copyable struct R {\n id: Id,\n n: Int,\n}"
= "pub copyable (struct R (id Id) (n Int))");
check "enum variant"
(one "pub enum E {\n A(Code),\n B,\n}" = "pub (enum E (A Code) (B))");
check "capability"
(one "pub capability C {\n fn m(id: Id) effects {C.m} -> R\n}"
= "pub (capability C (fn m (id:Id) [eset C.m] -> R decl))");
check "import와 reexport"
(List.length (parse_ok "import \"d/p\" as P\nreexport N\n").Ast.items = 2);
check "const" (one "pub const N: Int = 3" = "pub (const N:Int 3)")
(* --- 식 --- *)
let () =
check "우선순위"
(one "fn f() {\n a + b * c == d && e\n}"
= "(fn f () (block (&& (== (+ a (* b c)) d) e)))");
check "postfix 연쇄"
(one "fn f() {\n p.q(r)?.s\n}"
= "(fn f () (block (. (? (call (. p q) r)) s)))");
check "if는 식"
(one "fn f() {\n let x = if c {\n a\n } else {\n b\n }\n x\n}"
= "(fn f () (block (let x (if c (block a) (block b))) x))");
check "match"
(one "fn f() {\n match e {\n A(_) => 1,\n _ => 2,\n }\n}"
= "(fn f () (block (match e ((A _) => 1) (_ => 2))))");
check "scope"
(one "fn f() {\n scope sc {\n sc.spawn(g)\n }\n}"
= "(fn f () (block (scope sc (block (call (. sc spawn) g)))))");
check "struct 리터럴"
(one "fn f() {\n H { a: 1 }\n}" = "(fn f () (block (struct H (a 1))))")
(* --- 파서가 거부해야 하는 것 --- *)
let () =
check "파라미터 위치의 effect 합집합"
(parse_err "fn f(g: fn(a) effects e | {A.b})"
= Some "파라미터 위치의 effects 절에는 합집합을 쓸 수 없습니다 (변수 단독 또는 리터럴 집합만 가능)");
check "match 가드"
(parse_err
"fn f() {\n match e {\n A(r) if r.x => 1,\n _ => 2,\n }\n}"
= Some "match 가드는 v0에 없습니다 (분기 본문에서 if를 쓰십시오)");
check "후행 콤마 누락"
(parse_err "fn f(\n a: Int\n)" = Some "다중 줄 목록에는 후행 콤마가 필요합니다");
check "if 머리의 struct 리터럴"
(parse_err "fn f() {\n if H { a: 1 } {\n b\n }\n}" <> None);
check "대입 왼쪽 검사"
(parse_err "fn f() {\n g() = 1\n}" = Some "대입 왼쪽에는 변수나 필드만 올 수 있습니다");
check "한 줄에 문 두 개" (parse_err "fn f() {\n a b\n}" = Some "문 끝에 줄바꿈이 필요합니다")
(* --- 괄호 안에서는 struct 리터럴이 다시 허용된다 --- *)
let () =
check "괄호로 감싸면 if 머리에서도 가능"
(one "fn f() {\n if (H { a: 1 }).b {\n c\n }\n}"
= "(fn f () (block (if (. (struct H (a 1)) b) (block c))))")
(* --- 샘플: 01~07은 파싱되고 08은 거부되어야 한다 --- *)
let () =
let dir = "../samples" in
let files =
Sys.readdir dir |> Array.to_list
|> List.filter (fun f -> Filename.check_suffix f ".cool")
|> List.sort compare
in
List.iter
(fun f ->
let path = Filename.concat dir f in
let is_syntax_error_file = f = "08_syntax_errors.cool" in
match Driver.ast path with
| Ok m ->
if is_syntax_error_file then check (f ^ " 는 파서가 거부해야 한다") false
else check (f ^ " 구문 분석") (List.length m.Ast.items > 0)
| Error errors ->
if is_syntax_error_file then ()
else (
List.iter
(fun e -> Printf.printf " %s\n" (Driver.string_of_error e))
errors;
check (f ^ " 구문 분석") false))
files