Ocamlyacc
Ocamlyacc – generator parserów dla języka Ocaml, wzorowany na programach typu yacc dla C.
Operowanie na drzewach składniowych w Ocamlu jest o wiele łatwiejsze niż w C. Połączenie tych dwóch cech - generatora parserów i języka z dobrymi możliwościami przekształceń symbolicznych, czyni z Ocamla jeden z najwygodniejszych w użyciu języków pisania parserów.
Programy w ocamlyacc mają rozszerzenie .mly
Ocamlyacc używany jest zwykle w połączeniu z generatorem lekserów ocamllex, wzorowanym na programach typu lex dla C. Pliki źródłowe ocamllex mają rozszerzenie .mll
.
Przykład
edytujNapiszemy parser, który interpretuje pliki języka K, nazwanego tak ze względu na duże wizualne podobieństwo do C.
K nie jest zbyt ambitnym językiem (inaczej interpreter znacznie przerastałby parser), ale umożliwia czytanie wejścia (get
),
wykonywanie obliczeń (tutaj funkcja Fibonacciego) oraz drukowanie wyników (p
):
pre1 = 0; pre2 = 1; n = 0; get nmax; while (n < nmax) { new = pre1 + pre2; pre2 = pre1; pre1 = new; n += 1 }; p pre1
Lekser
edytujZawartość pliku lexer.mll
:
{ open Parser } let space = [' ' '\n' '\t'] let digit = ['0'-'9'] let alpha = ['a'-'z' 'A'-'Z'] rule token = parse space + { token lexbuf } | 'p' { PRINT } | "if" { IF } | "else" { ELSE } | "while" { WHILE } | "proc" { PROC } | "call" { CALL } | "get" { GET } | alpha (digit|alpha) * { ID (Lexing.lexeme lexbuf) } | digit + { INT (int_of_string (Lexing.lexeme lexbuf)) } | '+' { PLUS } | '-' { MINUS } | '*' { TIMES } | '(' { LPAREN } | ')' { RPAREN } | '{' { LBRACE } | '}' { RBRACE } | ';' { SEMI } | "==" { EQEQ } | '=' { EQ } | ">" { GT } | ">=" { GE } | "<" { LT } | "<=" { LE } | "+=" { PLUS_EQ } | "-=" { MINUS_EQ } | "*=" { TIMES_EQ } | eof { EOF }
Jak widać ogólna struktura prostego pliku .mll
to:
{ kod w ocamlu, przepisywany bezpośrednio na wyjście } let x = kilka_deklaracji let y = ułatwiających let z = pisanie_reguł rule token = parse reguła { wyrażenie_zwracające_symbol } | inna_reguła { inne_wyrażenie_zwracające_symbol } | itd { itd } | eof { można_dzięki_temu_przekazać_eof_do_parsera }
Parser
edytujPlik .mly
zaczyna się kodem, który będzie bezpośrednio przepisany, zawartym między %{
i %}
.
Potem następuje deklaracja typu tokenów - tokeny proste deklaruje się %token NAZWA
, tokeny niosące dodatkowe informacje
%token <typ> NAZWA
. Potem trzeba zdefiniować regułę startową %start reguła
i jej typ %type <typ> reguła
.
Typy innych reguł Ocaml potrafi zazwyczaj inferować, jednak można je też zdeklarować.
Potem następuje %%
i zaczynają się reguły postaci:
nazwa: możliwa struktura { wyrażenie dla tej struktury } | inna możliwa struktura { wyrażenie dla innej struktury } | itd { itd } ;;
Przykład parsera dla K:
%{ open Hashtbl;; open Program;; %} %token <string> ID %token <int> INT %token LPAREN RPAREN %token LBRACE RBRACE %token PLUS MINUS TIMES %token EQEQ GT GE LT LE %token PLUS_EQ MINUS_EQ TIMES_EQ %token IF ELSE WHILE %token SEMI %token EOF %token PRINT %token EQ %token PROC CALL GET %start program %type <Program.stree list> program %% program: decls_and_stmts EOF { $1 } ;; decl: PROC ID stmt {PROC_TREE ($2,$3)} ;; decls_and_stmts: decl { [$1] } | stmt { [$1] } | stmt SEMI decls_and_stmts { $1 :: $3 } | decl SEMI decls_and_stmts { $1 :: $3 } ;; stmtgroup: stmt { [$1] } | stmt SEMI stmtgroup { $1 :: $3 } ;; stmt: stmt_a1 { $1 } | stmt_a2 { $1 } ;; stmt_a1: /* empty */ { NOOP_TREE } | LBRACE stmtgroup RBRACE { STMT_GROUP_TREE $2 } | PRINT expr { PRINT_TREE $2 } | CALL ID { CALL_TREE $2 } | GET ID { GET_TREE $2 } | ID EQ expr { ASSIGN_TREE ($1,$3) } | ID PLUS_EQ expr { ASSIGN_TREE ($1,(PLUS_TREE((ID_TREE $1),$3))) } | ID MINUS_EQ expr { ASSIGN_TREE ($1,(MINUS_TREE((ID_TREE $1),$3))) } | ID TIMES_EQ expr { ASSIGN_TREE ($1,(TIMES_TREE((ID_TREE $1),$3))) } | WHILE LPAREN expr RPAREN stmt_a1 { WHILE_TREE($3,$5)} | IF LPAREN expr RPAREN stmt_a1 ELSE stmt_a1 { IF_TREE($3,$5,$7) } ;; stmt_a2: WHILE LPAREN expr RPAREN stmt_a2 { WHILE_TREE($3,$5)} | IF LPAREN expr RPAREN stmt { IF_TREE($3,$5,NOOP_TREE) } | IF LPAREN expr RPAREN stmt_a1 ELSE stmt_a2 { IF_TREE($3,$5,$7) } ;; expr: expr_p0 { $1 } ;; expr_p0: expr_p0 EQEQ expr_p1 { EQEQ_TREE ($1,$3) } | expr_p0 GT expr_p1 { GT_TREE ($1,$3) } | expr_p0 LT expr_p1 { GT_TREE ($3,$1) } | expr_p0 GE expr_p1 { GE_TREE ($1,$3) } | expr_p0 LE expr_p1 { GE_TREE ($3,$1) } | expr_p1 { $1 } ;; expr_p1: expr_p1 PLUS expr_p2 { PLUS_TREE ($1,$3) } | expr_p1 MINUS expr_p2 { MINUS_TREE ($1,$3) } | expr_p2 { $1 } ;; expr_p2: expr_p2 TIMES expr_p3 { TIMES_TREE ($1,$3) } | expr_p3 { $1 } ;; expr_p3: LPAREN expr_p0 RPAREN { $2 } | ID { ID_TREE $1 } | INT { INT_TREE $1 } ;;
W tym przypadku priorytety operatorów zdefiniowano bezpośrednio na poziomie gramatyki. Można użyć zamiast tego analogicznie jak w yacc deklaracji %left
, %right
, %nonassoc
i %prec
stmt_a1
i stmt_a2
rozwiązują interesujący dangling else problem i są warte specjalnej uwagi.
program.ml
edytujPlik program.ml
zawiera wnętrzności programu. Nie są one interesujące z punktu widzenia budowy parsera,
jednak należy je zamieścić ze względu na kompletność helloworlda.
Typy xtree
- drzewo przedstawiające wyrażenie i stree
- drzewo przedstawiające polecenie są używane przez parser.
(* autohash *) type autohash = { autohash_last: int ref; autohash_h:(string,int)Hashtbl.t; };; let autohash_get ah s = try Hashtbl.find ah.autohash_h s with Not_found -> ( ah.autohash_last := 1 + !(ah.autohash_last); Hashtbl.add ah.autohash_h s !(ah.autohash_last); !(ah.autohash_last); );; (* types *) type xtree = ID_TREE of string | INT_TREE of int | PLUS_TREE of xtree * xtree | MINUS_TREE of xtree * xtree | TIMES_TREE of xtree * xtree | EQEQ_TREE of xtree * xtree | GT_TREE of xtree * xtree | GE_TREE of xtree * xtree ;; type stree = ASSIGN_TREE of string * xtree | NOOP_TREE | IF_TREE of xtree * stree * stree | WHILE_TREE of xtree * stree | PRINT_TREE of xtree | STMT_GROUP_TREE of stree list | PROC_TREE of string * stree | CALL_TREE of string | GET_TREE of string ;; (* interpreter *) type ienv = { ienv_var : (string,int) Hashtbl.t; ienv_proc : (string,stree) Hashtbl.t; } let rec xtree_eval e = function INT_TREE a -> a | ID_TREE a -> (Hashtbl.find e.ienv_var a) | PLUS_TREE (a, b) -> (xtree_eval e a) + (xtree_eval e b) | MINUS_TREE (a, b) -> (xtree_eval e a) - (xtree_eval e b) | TIMES_TREE (a, b) -> (xtree_eval e a) * (xtree_eval e b) | EQEQ_TREE (a,b) -> let av = (xtree_eval e a) in let bv = (xtree_eval e b) in if (av = bv) then 1 else 0; | GT_TREE (a,b) -> let av = (xtree_eval e a) in let bv = (xtree_eval e b) in if (av > bv) then 1 else 0; | GE_TREE (a,b) -> let av = (xtree_eval e a) in let bv = (xtree_eval e b) in if (av >= bv) then 1 else 0; ;; let rec stmt_eval e = function PRINT_TREE a -> print_string "> "; print_int(xtree_eval e a); print_newline(); | IF_TREE (a,b,c) -> if ((xtree_eval e a) != 0) then (stmt_eval e b) else (stmt_eval e c); | NOOP_TREE -> () | ASSIGN_TREE (a,b) -> (Hashtbl.add e.ienv_var a (xtree_eval e b)) | WHILE_TREE (a,b) -> while ((xtree_eval e a) != 0) do stmt_eval e b done | STMT_GROUP_TREE (a) -> stmt_group_eval e a | PROC_TREE (a,b) -> (Hashtbl.add e.ienv_proc a b) | CALL_TREE (a) -> stmt_eval e (Hashtbl.find e.ienv_proc a) | GET_TREE (a) -> (Hashtbl.add e.ienv_var a (read_int())) and stmt_group_eval e = function [] -> () | h::t -> (stmt_eval e h); (stmt_group_eval e t) ;; let rec stmts_eval e = function [] -> () | h::t -> stmt_eval e h ; stmts_eval e t; ;; let program_eval p = let e = { ienv_var = Hashtbl.create 16; ienv_proc = Hashtbl.create 16} in stmts_eval e p ;;
Procedura główna
edytujPlik kk.ml
zawiera "procedurę główną" i wywołuje nasz parser.
open Program let _ = if Array.length Sys.argv <= 1 then print_string ("Usage: "^(Sys.argv.(0))^" <k program>\n") else let p = Parser.program Lexer.token (Lexing.from_channel (open_in Sys.argv.(1))) in program_eval p ;;
Makefile
edytujW tworzeniu Makefile
pomocny może być program ocamldep
, choć nie radzi on sobie bezpośrednio z plikami .mll
i .mly
.
all: kk kk_opt: program.cmx lexer.cmx parser.cmx kk.cmx ocamlopt -o $@ $^ kk: program.cmo lexer.cmo parser.cmo kk.cmo ocamlc -o $@ $^ %.ml: %.mll ocamllex $< %.mli %.ml: %.mly ocamlyacc $< %.cmo: %.ml ocamlc -c $< %.cmx: %.ml ocamlopt -c $< %.cmi: %.mli ocamlc -c $< kk.cmo: lexer.cmo parser.cmi program.cmo kk.cmx: lexer.cmx parser.cmx program.cmx lexer.cmo: parser.cmi lexer.cmx: parser.cmx parser.cmo: program.cmo parser.cmi parser.cmx: program.cmx parser.cmi parser.cmi: program.cmo
Użycie
edytuj$ ./kk fib.k 20 > 6765 $