ocamllex 개요
ocamllex 명령어는 정규 표현식 집합과 이에 연결된 의미 동작으로부터 어휘 분석기를 생성합니다. 입력 파일이 lexer.mll인 경우 다음 명령을 실행합니다:
ocamllex <i>lexer</i>.mll
이는 lexer.ml 파일에 어휘 분석기의 OCaml 코드를 생성합니다. 이 파일은 어휘 정의의 각 진입점마다 하나의 어휘 함수를 정의하며, 이러한 함수는 진입점과 동일한 이름을 가집니다. 어휘 함수는 어휘 버퍼를 인수로 받고 해당 진입점의 의미 속성을 반환합니다.
어휘 버퍼는 표준 라이브러리 모듈 Lexing에서 구현되는 추상 데이터 타입입니다. 함수 Lexing.from_channel, Lexing.from_string, Lexing.from_function은 각각 입력 채널, 문자열 또는 임의의 읽기 함수에서 읽는 어휘 버퍼를 생성합니다.
어휘 정의 문법
어휘 정의의 형식은 다음과 같습니다:
{ <i>header</i> }
let <i>ident</i> = <i>regexp</i> ...
rule <i>entrypoint</i> =
parse <i>regexp</i> { <i>action</i> }
| ...
| <i>regexp</i> { <i>action</i> }
and <i>entrypoint</i> =
parse ...
and ...
{ <i>trailer</i> }
주석은 OCaml에서와 같이 (*와 *)로 구분됩니다.
헤더 및 트레일러
header와 trailer 섹션은 중괄호로 묶인 임의의 OCaml 텍스트입니다. 둘 다 생략할 수 있습니다. 존재하는 경우 헤더 텍스트는 출력 파일의 시작 부분에 그대로 복사되고 트레일러 텍스트는 끝 부분에 복사됩니다. 일반적으로 헤더 섹션에는 동작에 필요한 open 지시문과 동작에서 사용되는 보조 함수가 포함됩니다.
정규 표현식 명명
헤더와 진입점 사이에서 자주 사용되는 정규 표현식에 이름을 부여할 수 있습니다. 이는 letident= regexp로 작성됩니다. 후속 정규 표현식에서 식별자 ident를 regexp의 축약형으로 사용할 수 있습니다.
진입점
진입점의 이름은 OCaml 값에 유효한 식별자(소문자로 시작)여야 합니다. 각 진입점은 Lexing.lexbuf 타입의 인수 하나를 취하는 OCaml 함수가 됩니다. 문자는 Lexing.lexbuf 인수에서 읽혀지고 규칙에서 제공된 정규 표현식과 비교되어 입력의 접두사가 규칙 중 하나와 일치할 때까지 진행됩니다. 그런 다음 해당 동작이 평가되어 함수의 결과로 반환됩니다.
여러 정규 표현식이 입력의 접두사와 일치하는 경우 "최장 일치" 규칙이 적용됩니다: 입력의 최장 접두사와 일치하는 정규 표현식이 선택됩니다. 동률의 경우 규칙에서 먼저 나타나는 정규 표현식이 선택됩니다.
정규 표현식
정규 표현식은 lex 스타일이며 OCaml과 유사한 문법을 가집니다.
'char'- Objective Caml 문자 상수와 동일한 문법을 가진 문자 상수입니다. 지정된 문자와 일치합니다.
_- (밑줄.) 모든 문자와 일치합니다.
eof- 어휘 분석기 입력의 끝과 일치합니다. 참고: 일부 시스템에서는 대화형 입력에서 파일 끝 다음에 더 많은 문자가 올 수 있습니다. 그러나
ocamllex는eof뒤에 다른 것이 오는 정규 표현식을 올바르게 처리하지 못합니다. "string"- Objective Caml 문자열 상수와 동일한 문법을 가진 문자열 상수입니다. 해당 문자 시퀀스와 일치합니다.
[character-set]- 지정된 문자 집합에 속하는 단일 문자와 일치합니다. 유효한 문자 집합은: 단일 문자 상수
'c'; 문자 범위'c1'-'c2'(c1과 c2 사이의 모든 문자, 포함); 두 개 이상의 문자 집합의 결합(연결로 표시). [^character-set]- 지정된 문자 집합에 속하지 않는 단일 문자와 일치합니다.
regexp*- (반복.)
regexp와 일치하는 0개 이상의 문자열의 연결과 일치합니다. regexp+- (엄격한 반복.)
regexp와 일치하는 1개 이상의 문자열의 연결과 일치합니다. regexp?- (옵션.) 빈 문자열 또는
regexp와 일치하는 문자열과 일치합니다. regexp1|regexp2- (대안.)
regexp1 또는regexp2와 일치하는 문자열과 일치합니다. regexp1regexp2- (연결.) 두 문자열의 연결과 일치합니다. 첫 번째는
regexp1과 일치하고 두 번째는regexp2와 일치합니다. (regexp)regexp와 동일한 문자열과 일치합니다.ident- 이전
letident=regexp정의에 의해ident에 바인딩된 정규 표현식을 참조합니다.
연산자의 우선순위에 관해서는 *와 +가 가장 높은 우선순위를 가지며, 그 다음은 ?, 연결, 마지막으로 |(대안)입니다.
동작
동작은 임의의 OCaml 표현식입니다. 식별자 lexbuf가 현재 어휘 버퍼에 바인딩된 컨텍스트에서 평가됩니다. lexbuf의 일반적인 사용법은 다음과 같습니다:
Lexing.lexeme lexbuf- 일치한 문자열을 반환합니다.
Lexing.lexeme_char lexbufn- 일치한 문자열의 n번째 문자를 반환합니다. 첫 번째 문자는 n = 0에 해당합니다.
Lexing.lexeme_start lexbuf- 일치한 문자열의 시작 위치를 입력 텍스트에서 절대 위치로 반환합니다. 입력 텍스트에서 읽은 첫 번째 문자는 위치 0을 가집니다.
Lexing.lexeme_end lexbuf- 일치한 문자열의 끝 위치를 입력 텍스트에서 절대 위치로 반환합니다. 입력 텍스트에서 읽은 첫 번째 문자는 위치 0을 가집니다.
- entrypoint
lexbuf - (entrypoint는 동일한 어휘 정의의 다른 진입점 이름임) 주어진 진입점에서 어휘 분석기를 재귀적으로 호출합니다. 중첩된 주석을 어휘 분석할 때 유용합니다.
예약 식별자
__ocaml_lex로 시작하는 모든 식별자는 ocamllex에서 사용하기 위해 예약되어 있으므로 프로그램에서 이러한 식별자를 사용하지 마십시오.
ocamlyacc 개요
ocamlyacc 명령어는 의미 동작이 첨부된 문맥 자유 문법 명세로부터 구문 분석기를 생성합니다. 입력 파일이 grammar.mly인 경우 다음 명령을 실행합니다:
ocamlyacc <i>options</i> <i>grammar</i>.mly
이는 grammar.ml 파일에 구문 분석기의 OCaml 코드를 생성하고 grammar.mli 파일에 인터페이스를 생성합니다.
생성된 모듈은 문법의 각 진입점마다 하나의 구문 분석 함수를 정의합니다. 이러한 함수는 진입점과 동일한 이름을 가집니다. 구문 분석 함수는 어휘 분석기(어휘 버퍼에서 토큰으로의 함수)와 어휘 버퍼를 인수로 받고 해당 진입점의 의미 속성을 반환합니다. 어휘 분석기 함수는 일반적으로 ocamllex 프로그램에 의해 어휘 명세로부터 생성됩니다. 어휘 버퍼는 표준 라이브러리 모듈 Lexing에서 구현되는 추상 데이터 타입입니다. 토큰은 ocamlyacc가 생성한 인터페이스 파일 grammar.mli에서 정의된 구체적 타입 token의 값입니다.
문법 정의 문법
문법 정의는 다음과 같은 형식을 가집니다:
%{
<i>header</i>
%}
<i>declarations</i>
%%
<i>rules</i>
%%
<i>trailer</i>
주석은 "선언"과 "규칙" 섹션에서는 /*와 */(C에서와 같이)로, "헤더"와 "트레일러" 섹션에서는 (*와 *)(OCaml에서와 같이)로 묶입니다.
헤더 및 트레일러
헤더와 트레일러 섹션은 grammar.ml 파일에 그대로 복사되는 OCaml 코드입니다. 두 섹션 모두 선택 사항입니다. 헤더는 출력 파일의 시작 부분에 위치하며 일반적으로 규칙의 의미 동작에 필요한 open 지시문과 보조 함수를 포함합니다. 트레일러는 출력 파일의 끝 부분에 위치합니다.
선언
선언은 한 줄에 하나씩 주어집니다. 모두 % 기호로 시작합니다.
%tokensymbol...symbol- 주어진 심볼을 토큰(종료 심볼)으로 선언합니다. 이러한 심볼은
token구체적 타입의 상수 생성자로 추가됩니다. %token<type>symbol...symbol- 주어진 타입의 첨부 속성이 있는 토큰으로 주어진 심볼을 선언합니다. 이러한 심볼은 주어진 타입의 인수가 있는 생성자로
token구체적 타입에 추가됩니다.type부분은 임의의 OCaml 타입 표현식이지만, 모든 타입 생성자 이름은 완전히 정규화되어야 합니다(예:Modname.typename). 이는 헤더 섹션에서 적절한open지시문(예:open Modname)이 주어졌더라도 표준 내장 타입을 제외한 모든 타입에 대해 그렇습니다. 이는 헤더가.ml출력 파일에만 복사되지만.mli출력 파일에는 복사되지 않기 때문이며,%token선언의type부분은 둘 다에 복사되기 때문입니다. %startsymbol...symbol- 주어진 심볼을 문법의 진입점으로 선언합니다. 각 진입점에 대해 동일한 이름의 구문 분석 함수가 출력 모듈에 정의됩니다. 진입점으로 선언되지 않은 비종료 기호는 그러한 구문 분석 함수를 가지지 않습니다. 시작 심볼은 아래의
%type지시문으로 타입을 부여해야 합니다. %type<type>symbol...symbol- 주어진 심볼의 의미 속성 타입을 지정합니다. 시작 심볼에만 필수입니다. 다른 비종료 기호는 수동으로 타입을 부여할 필요가 없습니다: 이러한 타입은 Objective Caml 컴파일러를 통해 출력 파일을 실행할 때 추론됩니다(단,
-s옵션이 적용되지 않는 한).type부분은 임의의 OCaml 타입 표현식이지만 위에서 설명한%token과 동일하게 모든 타입 생성자 이름은 완전히 정규화되어야 합니다. %leftsymbol...symbol%rightsymbol...symbol%nonassocsymbol...symbol- 주어진 심볼에 우선순위와 결합성을 연결합니다. 같은 줄의 모든 심볼은 동일한 우선순위를 가집니다. 이들은
%left,%right또는%nonassoc줄에서 이전에 선언된 심볼보다 높은 우선순위를 가집니다. 이들은%left,%right또는%nonassoc줄에서 이후에 선언된 심볼보다 낮은 우선순위를 가집니다. 심볼은 왼쪽(%left), 오른쪽(%right) 또는 비결합적(%nonassoc)으로 결합한다고 선언됩니다. 심볼은 일반적으로 토큰입니다. 규칙 내에서%prec지시문과 함께 사용하기 위한 더미 비종료 기호일 수도 있습니다.
규칙
규칙의 문법은 다음과 같습니다:
<i>nonterminal</i> :
<i>symbol</i> ... <i>symbol</i> { <i>semantic-action</i> }
| ...
| <i>symbol</i> ... <i>symbol</i> { <i>semantic-action</i> }
;
규칙은 또한 우변 부분에 %prec symbol 지시문을 포함할 수 있으며, 이는 기본 우선순위와 결합성을 주어진 심볼의 우선순위와 결합성으로 재정의합니다.
의미 동작은 정의된 비종료 기호에 첨부된 의미 속성을 생성하기 위해 평가되는 임의의 OCaml 표현식입니다. 의미 동작은 $ 표기법을 사용하여 규칙의 우변에 있는 심볼의 의미 속성에 접근할 수 있습니다: $1은 첫 번째(왼쪽) 심볼의 속성이고, $2는 두 번째 심볼의 속성 등입니다.
규칙은 yacc에서와 같이 재동기화 지점을 나타내기 위해 특수 심볼 error를 포함할 수 있습니다.
규칙 중간에 발생하는 동작은 지원되지 않습니다.
비종료 기호는 일반 OCaml 심볼과 유사하지만 '(단일 따옴표)로 끝날 수 없습니다.
오류 처리
오류 복구는 다음과 같이 지원됩니다: 구문 분석기가 오류 상태(적용 가능한 문법 규칙 없음)에 도달하면 "syntax error" 문자열을 인수로 하여 parse_error라는 함수를 호출합니다. 기본 parse_error 함수는 아무것도 하지 않고 반환하므로 오류 복구를 시작합니다(아래 참조). 사용자는 문법 파일의 헤더 섹션에서 사용자 정의 parse_error 함수를 정의할 수 있습니다.
구문 분석기의 문법 동작 중 하나가 Parsing.Parse_error 예외를 발생시키면 구문 분석기도 오류 복구 모드로 들어갑니다.
오류 복구 모드에서 구문 분석기는 오류 토큰을 시프트할 수 있는 위치에 도달할 때까지 스택에서 상태를 폐기합니다. 그런 다음 입력에서 세 개의 연속된 토큰을 찾을 때까지 토큰을 폐기하고 첫 번째 토큰으로 처리를 시작합니다. 오류 토큰을 시프트할 수 있는 상태를 복구할 수 없는 경우 구문 분석기는 Parsing.Parse_error 예외를 발생시켜 중단됩니다.
오류 복구 사용 방법에 대한 자세한 내용과 지침은 yacc 문서를 참조하십시오.
옵션
ocamlyacc 명령어는 다음 옵션을 인식합니다:
-v- 구문 분석 테이블의 설명과 문법의 모호성으로 인한 충돌 보고서를 생성합니다. 설명은 grammar
.output파일에 저장됩니다. -bprefix- 기본 명명 규칙 대신 출력 파일을 prefix
.ml, prefix.mli, prefix.output으로 이름 지정합니다.
런타임에 ocamlyacc로 생성된 구문 분석기는 OCAMLRUNPARAM 환경 변수에서 p 옵션을 설정하여 디버깅할 수 있습니다(섹션 10.2 참조). 이는 구문 분석기를 실행하는 푸시다운 오토마톤이 자신의 동작(토큰 시프트, 규칙 축소 등)을 출력하도록 합니다. 추적은 ocamlyacc -v로 생성된 grammar.output 파일을 확인하여 해석할 수 있는 규칙 번호와 상태 번호를 언급합니다.
완전한 예제
모든 시간의 즐겨찾기: 데스크 계산기. 이 프로그램은 표준 입력에서 산술 표현식을 한 줄씩 읽고 그 값을 출력합니다. 다음은 문법 정의입니다:
/* File parser.mly */
%token <int> INT
%token PLUS MINUS TIMES DIV
%token LPAREN RPAREN
%token EOL
%left PLUS MINUS /* lowest precedence */
%left TIMES DIV /* medium precedence */
%nonassoc UMINUS /* highest precedence */
%start main /* the entry point */
%type <int> main
%%
main:
expr EOL { $1 }
;
expr:
INT { $1 }
| LPAREN expr RPAREN { $2 }
| expr PLUS expr { $1 + $3 }
| expr MINUS expr { $1 - $3 }
| expr TIMES expr { $1 * $3 }
| expr DIV expr { $1 / $3 }
| MINUS expr %prec UMINUS { - $2 }
;
다음은 해당 어휘 분석기의 정의입니다:
(* File lexer.mll *)
{
open Parser (* The type token is defined in parser.mli *)
exception Eof
}
rule token = parse
[' ' '\t'] { token lexbuf } (* skip blanks *)
| ['\n' ] { EOL }
| ['0'-'9']+ { INT(int_of_string(Lexing.lexeme lexbuf)) }
| '+' { PLUS }
| '-' { MINUS }
| '*' { TIMES }
| '/' { DIV }
| '(' { LPAREN }
| ')' { RPAREN }
| eof { raise Eof }
다음은 구문 분석기와 어휘 분석기를 결합하는 메인 프로그램입니다:
(* File calc.ml *)
let _ =
try
let lexbuf = Lexing.from_channel stdin in
while true do
let result = Parser.main Lexer.token lexbuf in
print_int result; print_newline(); flush stdout
done
with Lexer.Eof ->
exit 0
모든 것을 컴파일하려면 다음을 실행합니다:
ocamllex lexer.mll # generates lexer.ml
ocamlyacc parser.mly # generates parser.ml and parser.mli
ocamlc -c parser.mli
ocamlc -c lexer.ml
ocamlc -c parser.ml
ocamlc -c calc.ml
ocamlc -o calc lexer.cmo parser.cmo calc.cmo
일반적인 오류
- ocamllex: transition table overflow, automaton is too big
-
ocamllex가 생성하는 결정적 오토마톤은 최대 32767개의 전이로 제한됩니다. 위 메시지는 어휘 정의가 너무 복잡하여 이 한계를 초과했음을 나타냅니다. 이는 일반적으로 다음과 같은 예제에서와 같이 언어의 알파벳 키워드 각각에 대한 별도의 규칙을 가진 어휘 정의로 인해 발생합니다.rule token = parse "keyword1" { KWD1 } | "keyword2" { KWD2 } | ... | "keyword100" { KWD100 } | ['A'-'Z' 'a'-'z'] ['A'-'Z' 'a'-'z' '0'-'9' '_'] * { IDENT(Lexing.lexeme lexbuf) }생성된 오토마톤을 작게 유지하려면 다음과 같이 일반적인 "식별자" 규칙 하나만으로 정의를 다시 작성하고 해시 테이블 조회를 통해 키워드와 식별자를 분리합니다:
{ let keyword_table = Hashtbl.create 53 let _ = List.iter (fun (kwd, tok) -> Hashtbl.add keyword_table kwd tok) [ "keyword1", KWD1; "keyword2", KWD2; ... "keyword100", KWD100 ] } rule token = parse ['A'-'Z' 'a'-'z'] ['A'-'Z' 'a'-'z' '0'-'9' '_'] * { let id = Lexing.lexeme lexbuf in try Hashtbl.find keyword_table s with Not_found -> IDENT s }