grammar: 문법 문서를 기계가 읽는 소스로 — LL(1)이 처음으로 검증된다
설명서와 파서가 어긋나 있었다. 그냥 어긋난 게 아니라, 그 설명서를 안 읽고
"else if가 안 된다"고 마찰 보고서에 잘못 적었다 — 설명서로서 제 역할을 한
번도 못 했다는 뜻이다.
lib/ebnf.ml: EBNF를 데이터로 읽는다. nullable, FIRST, FOLLOW를 계산하고
LL(1) 충돌을 보고한다. 매개변수 프로덕션(name<p>)을 지원한다 — expr_ns와
목록을 복제 없이 적기 위한 것이다.
문법 첫머리의 "설계 제약: LL(1)"은 지금까지 사람의 주장이었다. 기계로 재니
충돌 18건이 나왔다. 세 부류였다:
- 구조적 3건: stmt(assign/expr), primary(ident/struct_lit), pattern —
파서는 왼쪽 인수분해를 손으로 했는데 문법에 안 적혀 있었다
- 후행 콤마 9건: X , { "," , X } , [ "," ]는 콤마를 본 시점에 갈리지 않는다.
우재귀로 다시 적었다
- 줄바꿈 흡수 6건: 어느 쪽이 먹어도 파스 트리가 같다. greedy 규칙을 표기에
명시하고 그렇게 해소되는 것만 따로 분류한다
지금은 진짜 충돌 0건이고, 테스트가 이를 고정한다.
병렬 조사에서 나온 드리프트도 모두 반영했다:
- named_type, name_pattern에 한정 이름(Shapes.Shape, Shapes.Circle)
- name_pattern이 인자 0개와 괄호 없는 한정 생성자를 받는다
- string_lit의 이스케이프
- cap_method의 gen_params (파서가 이미 허용하고 있었다)
- field/variant/field_init/arm 사이의 콤마는 필수다
- stmt 사이의 NEWLINE도 필수다 — 선택적으로 적었더니 그것 하나가 LL(1)
충돌 셋을 만들었다
- { NEWLINE }을 전부 [ NEWLINE ]으로. 렉서가 연속 줄바꿈을 만들 수 없다
expr_ns는 산문 주석이었고 파서 상태 플래그로 구현돼 있었다. 매개변수
프로덕션으로 형식화해 문맥자유가 됐다.
렉서 쪽: Token.next_kind가 모든 토큰을 한 번씩 잇는다. 나열이 빠지면
컴파일러가 지적한다. 문법 문서의 키워드 표와 줄 끝 판정 목록은 이제
lib/lexical_doc.ml이 거기서 생성하고 테스트가 대조한다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019ZVDeU6KLuUVL3gs18Hm3E
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
(* 문법 파일을 읽어 기계가 소비할 수 있는지 확인하는 도구. *)
|
||||
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 () =
|
||||
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
|
||||
Reference in New Issue
Block a user