lexer: 어휘 분석 구현과 ASI 함정 하나 정정
grammar.ebnf의 어휘 절을 구현한다. 렉서는 선읽기를 요구하지 않고, 문법과 얽히는 유일한 부분인 NEWLINE 삽입은 can_end_statement 하나로 판정한다. 모든 토큰이 위치를 들고 다닌다 — 진단 품질이 헌법급이므로 나중에 붙이지 않는다. 샘플을 실제로 렉싱해 함정을 하나 잡았다. effects 절이 줄 끝에 오면 "}"가 값 종료 토큰이라 NEWLINE이 삽입되어 다음 줄의 "->"와 끊긴다(Go ASI와 같은 형태). 렉서에 문맥을 주는 대신 — 렉서 피드백은 철학 2가 배제한다 — 시그니처 머리의 흡수 위치를 프로덕션에 명시적으로 적어 닫았다. 파서가 임의로 건너뛰는 것이 아니라 문법에 적힌 자리에서만 흡수한다. 다중 줄 목록의 후행 콤마 필수도 함께 명시(콤마로 끝난 줄은 NEWLINE을 만들지 않으므로 목록이 자연히 이어진다). cool check는 어휘 분석을 돌리되 통과했다고 말하지 않는다. 파이프라인의 나머지가 없는 이상 그 파일은 검사된 것이 아니다. 디버깅용 cool tokens 추가. 테스트 30건: 키워드, 두 글자 연산자 우선, NEWLINE 삽입 6가지 경우, 리터럴과 이스케이프, 오류 7종과 오류 위치, 그리고 samples/*.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:
+217
-5
@@ -1,8 +1,220 @@
|
||||
let () = assert (String.length Coollang.Version.string > 0)
|
||||
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 () =
|
||||
match Coollang.Driver.check [] with
|
||||
| Error [ _ ] -> ()
|
||||
| _ -> failwith "빈 입력에는 오류가 나야 한다"
|
||||
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 () = print_endline "ok"
|
||||
(* --- 두 글자 연산자를 먼저 본다 --- *)
|
||||
|
||||
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;
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user