Files
coollang/test/test_coollang.ml
T
coolguyandClaude Opus 5 66a2cc6959 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
2026-08-30 02:14:37 +09:00

221 lines
5.6 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;
])