Files
coollang/tools/ebnf_tool.ml
T
coolguyandClaude Opus 5 61b1920909 recognize: 문법이 직접 읽는 인식기 — 파서와 판정을 대조한다
lib/recognize.ml은 docs/grammar.ebnf를 그대로 해석해 토큰 열을 받아들일지
판정한다. AST를 만들지 않는다 — 판정만 하므로 손 파서의 진단은 그대로
남는다.

문법이 LL(1)임을 이미 검증했으므로 선택이 결정적이다. 다음 토큰이 어느
대안의 FIRST에 있는지만 보고 되돌아가지 않는다.

같은 토큰 열을 손 파서와 인식기 양쪽에 주고 판정이 갈리는지 테스트가
검사한다. 저장소의 .cool 25개 전부에서 일치한다. 갈리면 빌드가 깨진다.

이제 "설명서를 잘 관리하자"에 기대지 않는다. 그 방법은 이미 실패했다 —
설명서가 어긋났고 아무도 몰랐고 나조차 안 읽었다.

확인: 문법에서 한정 이름(named_type의 "." ident)을 빼 보면 즉시
"파서는 받고 문법은 거부"가 두 파일에서 잡힌다.

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

122 lines
5.2 KiB
OCaml

(* 문법 파일을 읽어 기계가 소비할 수 있는지 확인하는 도구. *)
let read file =
let ic = open_in_bin file in
let n = in_channel_length ic in
let s = really_input_string ic n in
close_in ic;
s
(* 문법과 손 파서를 같은 파일에 돌려 판정이 갈리는지 본다 *)
let compare_files files =
let g =
match Coollang.Ebnf.parse_result (read "docs/grammar.ebnf") with
| Ok g -> g
| Error e -> failwith (Printf.sprintf "문법 %d행: %s" e.line e.msg)
in
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "char"; "digit"; "letter" ]
in
let bad = ref 0 in
List.iter
(fun f ->
match Coollang.Lexer.lex_result (read f) with
| Error e -> Printf.printf " 렉서 오류 %s:%d:%d %s\n" f e.pos.line e.pos.col e.msg
| Ok toks ->
let hand =
match Coollang.Parser.parse_result toks with
| Ok _ -> None
| Error e -> Some (e.pos, e.msg)
in
let spec =
match Coollang.Recognize.check ~tokens g toks with
| Ok () -> None
| Error e ->
Some (e.pos, Printf.sprintf "%s이(가) 필요한데 %s" e.expected e.got)
in
(match (hand, spec) with
| None, None -> ()
| Some _, Some _ -> ()
| None, Some (p, m) ->
incr bad;
Printf.printf "갈림 %s: 파서는 받고 문법은 거부 (%d:%d %s)\n" f
p.Coollang.Token.line p.Coollang.Token.col m
| Some (p, m), None ->
incr bad;
Printf.printf "갈림 %s: 문법은 받고 파서는 거부 (%d:%d %s)\n" f
p.Coollang.Token.line p.Coollang.Token.col m))
files;
Printf.printf "\n파일 %d개 중 판정이 갈린 것 %d개\n" (List.length files) !bad;
if !bad > 0 then exit 1
let () =
if Array.length Sys.argv > 2 && Sys.argv.(1) = "--compare" then (
compare_files (Array.to_list (Array.sub Sys.argv 2 (Array.length Sys.argv - 2)));
exit 0);
if Array.length Sys.argv > 1 && Sys.argv.(1) = "--lexical" then (
print_string (Coollang.Lexical_doc.render ());
print_newline ();
exit 0);
let file = if Array.length Sys.argv > 1 then Sys.argv.(1) else "docs/grammar.ebnf" in
match Coollang.Ebnf.parse_result (read file) with
| Error e -> Printf.printf "%s:%d: %s\n" file e.line e.msg
| Ok g ->
Printf.printf "프로덕션 %d개\n" (List.length g);
Printf.printf "\n정의되지 않은 채 참조된 이름:\n";
List.iter (fun n -> Printf.printf " %s\n" n) (Coollang.Ebnf.undefined g);
Printf.printf "\n어디서도 참조되지 않는 프로덕션 (시작 기호 module 제외):\n";
List.iter
(fun n -> Printf.printf " %s\n" n)
(Coollang.Ebnf.unreachable g ~start:"module");
(* 문법이 쓰는 단말 전부. 렉서가 만드는 토큰과 대조하기 위한 것. *)
let terms = Hashtbl.create 64 in
let rec walk : Coollang.Ebnf.expr -> unit = function
| Term s -> Hashtbl.replace terms s ()
| Ref _ | RefArg _ -> ()
| Seq xs | Alt xs -> List.iter walk xs
| Opt e | Rep e -> walk e
| Except (a, b) -> walk a; walk b
in
List.iter (fun (r : Coollang.Ebnf.rule) -> walk r.body) g;
let ts = Hashtbl.fold (fun k () acc -> k :: acc) terms [] |> List.sort compare in
Printf.printf "\n문법이 쓰는 단말 %d개:\n %s\n" (List.length ts)
(String.concat " " ts);
(* 어휘 절의 이름은 파서 층에서 단말이다 *)
let tokens =
[ "ident"; "int_lit"; "string_lit"; "NEWLINE"; "letter"; "digit"; "char" ]
in
let g = Coollang.Ebnf.expand g in
let a = Coollang.Ebnf.analyze ~tokens g in
Printf.printf "\n비어도 되는(nullable) 프로덕션:\n %s\n"
(String.concat " "
(List.filter_map
(fun (r : Coollang.Ebnf.rule) ->
if Coollang.Ebnf.nullable a r.name then Some r.name else None)
g));
Printf.printf "\nFIRST 표본:\n";
List.iter
(fun n ->
Printf.printf " %-14s %s\n" n
(String.concat " "
(Coollang.Ebnf.SS.elements (Coollang.Ebnf.first a n))))
[ "decl"; "stmt"; "primary"; "type"; "pattern"; "item" ];
let cs = Coollang.Ebnf.conflicts ~tokens ~greedy:[ "NEWLINE" ] g in
let real = List.filter (fun (c : Coollang.Ebnf.conflict) -> not c.c_greedy) cs in
let soft = List.filter (fun (c : Coollang.Ebnf.conflict) -> c.c_greedy) cs in
Printf.printf "\n== LL(1) 충돌: 진짜 %d건, greedy로 해소 %d건 ==\n"
(List.length real) (List.length soft);
List.iter
(fun (c : Coollang.Ebnf.conflict) ->
Printf.printf "\n%s (%s:%d) [%s]\n 겹치는 토큰: %s\n %s\n" c.c_rule file
c.c_line c.c_kind
(String.concat " " c.c_tokens)
c.c_detail)
real;
if soft <> [] then begin
Printf.printf "\n-- greedy로 해소되는 것 --\n";
List.iter
(fun (c : Coollang.Ebnf.conflict) ->
Printf.printf " %s:%d %s [%s]\n" file c.c_line c.c_rule
(String.concat " " c.c_tokens))
soft
end