<プログラム> | ::= | <式> |
<式> | ::= | <(0以上の)整数> |
| | <識別子> | |
| | (<プリミティブ> <式1> … <式n>) | |
<プリミティブ> | ::= | + | − | * | add1 | sub1 |
3 x (+ 3 x') (add1 (+ 3 x1))それでは,構文に関する部分から順に,5つのファイルを見ていく.
id は変数の識別情報を示すための型で,ここでは変数の名前を表す文字列 としている.(より現実的なインタプリタ・コンパイラでは,変数の型などの 情報も加わることが多い.) prim,exp,program 型に関しては 上の文法構造をそのまま写した形の宣言になっていることがわかるだろう.
%{ <ヘッダ> %} <宣言> %% <文法規則> %% <トレイラ><ヘッダ>, <トレイラ> は Objective Caml のプログラムを 書く部分で,ocamlyacc が生成する parser.ml の,それぞれ先頭・ 末尾にそのまま埋め込まれる.<宣言>はトークン(終端記号)や, 開始記号,優先度などの宣言を行う.parser.mly では演習を通して, 開始記号とトークンの宣言のみを使用する.<文法規則>には 文法記述と還元時のアクションを記述する.ヘッダ・トレイラでは, コメントは Objective Caml と同様 (* ... *) であり,宣言・文法規則部分では C 言語と同様な記法 /* ... */ で記述する.
この文法定義ファイルではトレイラは空になっていて,その前の%% は省略されている.
%{ open Syntax %} %token LPAREN RPAREN %token PLUS MINUS MUL ADD1 SUB1 %token <int> INTV %token <Syntax.id> ID %start toplevel %type <Syntax.program> toplevel %% toplevel : Exp { Prog $1 } Exp : INTV { ILit $1 } | ID { Var $1 } | LPAREN PrimOp Arglist RPAREN { Prim ($2, $3) } PrimOp : PLUS { Plus } | MINUS { Minus } | MUL { Mul } | ADD1 { Add1 } | SUB1 { Sub1 } Arglist : /* empty */ { [] } | Exp Arglist { $1 :: $2 }
Figure 2: mini Scheme1 インタプリタ: parser.mly
<非終端記号名> : <記号名11> ... <記号名1n1> { <還元時アクション1> } | <記号名21> ... <記号名2n2> { <還元時アクション2> } ...のように記述する.<還元時アクション>には Objective Caml の式を記述する. $i で,i番目の記号の属性を参照することができる.式全体の評価結果がこ の非終端記号の属性となるので,式の型は全て一致している必要がある.例え ば Exp は Syntax.exp 型の値(つまり mini Scheme1 の式の抽象構文木)を属 性として持ち,各アクションでは,その生成規則に対応する抽象構文木を生成 するような式が書かれている.
{ <ヘッダ> } let <名前> = <正則表現> ... rule <エントリポイント> = parse <正則表現> { <アクション> } | <正則表現> { <アクション> } | ... and <エントリポイント> = parse ... and ... { <トレイラ> }という構成になっている.ヘッダ・トレイラ部には,Objective Caml のプログラム を書くことができ,ocamllex が生成する lexer.ml ファイルの先頭・末尾 に埋め 込まれる.次の let を使った定義部は,よく使う正則表現に名前を つけるための部分で,lexer.mll では何も定義されていない.続く部分がエ ントリポイント,つまり字句解析の規則の定義で,同名の関数が ocamllex に よって生成される.規則としては正則表現とそれにマッチした際のアクション を(Objective Caml 式で)記述する.アクションは,基本的には(parser.mly で宣 言された)トークン(Parser.token 型)を返すような式を記述する.また,字 句解析に使用する文字列バッファが lexbuf という名前で使えるが,通常は 以下の使用法でしか使われない.
ヘッダ部では,予約語の文字列と,それに対応するトークンの連想リストであ る,reservedWords を定義している.後でみるように,List.assoc関数を 使って,文字列からトークンを取り出すことができる.
{ let reservedWords = [ (* Keywords *) ("+", Parser.PLUS); ("-", Parser.MINUS); ("*", Parser.MUL); ("add1", Parser.ADD1); ("sub1", Parser.SUB1); ] } rule main = parse (* ignore spacing and newline characters *) [' ' '\009' '\012' '\n']+ { main lexbuf } | ['0'-'9']+ { Parser.INTV (int_of_string (Lexing.lexeme lexbuf)) } | "(" { Parser.LPAREN } | ")" { Parser.RPAREN } | ['a'-'z'] ['a'-'z' '_' '0'-'9' '']* | ['+' '-' '*'] { let id = Lexing.lexeme lexbuf in try List.assoc id reservedWords with _ -> Parser.ID id } | eof { exit 0 }
Figure 3: mini Scheme1 インタプリタ: lexer.mll
Expressed Value | = | 整数 (…, −2, −1, 0, 1, 2, 3, …) |
Denoted Value | = | 整数 |
(* Expressed values *) type exval = IntV of int (* Denoted values *) and dnval = exvalexval 型はコンストラクタがひとつのヴァリアント型で表現しているが, これは,将来,式の値の集合に整数以外のものが入ってきたときの, コードの変更を容易にするためである.
val empty_env : unit -> env val extend_env : Syntax.id list -> dnval list -> env -> env val apply_env : Syntax.id -> env -> dnval exception UnboundVar of string最初の関数 empty_env は,empty_env () とすると,何の変数も束縛され ていない,空の環境を生成する.次の extend_env は,環境に新しい束縛 をいくつか同時に付け加えるための関数で,extend_env ids dnvals env で,環 境 env に対して,変数名のリストids のi番目の要素である 変数を, denoted value のリスト dnvalsの i 番目の要素に束縛した ような新しい環境を表す.最後の apply_env 関数は,環境から変数が束縛 された値を取り出すもので,apply_env id env で,環境 env の中を, 新しく加わった束縛から順に変数 id を探し,束縛されている値を返す. 変数が環境中に無い場合は,例外 UnboundVar が発行される.
また,プログラム実行開始時の環境(大域環境)を i,v,x が それぞれ 1,5,10 に束縛されたような環境として
type env = EmptyEnv | ExtendEnv of id list * dnval array * env exception UnboundVar of string let empty_env () = EmptyEnv let extend_env ids dnvals env = (* assumes List.length syms = List.length dnvals *) ExtendEnv (ids, Array.of_list dnvals, env) let rec list_pos n = function [] -> raise Not_found | m :: rest -> if n = m then 0 else succ (list_pos n rest) let rec apply_env id = function EmptyEnv -> raise (UnboundVar id) | ExtendEnv (ids, dnvals, rest) -> (try dnvals.(list_pos id ids) with Not_found -> apply_env id rest)
Figure 4: mini Scheme1 インタプリタ: 環境の実装 (core.ml)
let global_env = extend_env ["i"; "v"; "x"] (List.map (fun i -> IntV i) [1; 5; 10]) (empty_env())と定義する.この大域環境は主に変数参照のテスト用で,(空でなければ)何 でもよい.
let apply_prim p args = match p, args with | (Plus, [IntV i; IntV j]) -> IntV (i + j) | (Plus, _) -> failwith "Arity mismatch: +" | (Minus, [IntV i; IntV j]) -> IntV (i - j) | (Minus, _) -> failwith "Arity mismatch: -" | (Mul, [IntV i; IntV j]) -> IntV (i * j) | (Mul, _) -> failwith "Arity mismatch: *" | (Add1, [IntV i]) -> IntV (i + 1) | (Add1, _) -> failwith "Arity mismatch: add1" | (Sub1, [IntV i]) -> IntV (i - 1) | (Sub1, _) -> failwith "Arity mismatch: sub1" let rec eval_exp env = function ILit i -> IntV i | Var sym -> apply_env sym env | Prim (p, es) -> let args = eval_rands env es in apply_prim p args and eval_rands env = function [] -> [] | e :: rest -> eval_exp env e :: eval_rands env rest let eval_program (Prog e) = eval_exp global_env e
Figure 5: mini Scheme1 インタプリタ: 評価部の実装(core.ml)
(- (sub1 (* iii (+ ii v))) iv)などを試してみよ.
⇒ (+ 2 3 4) 9 ⇒ (* 3 4 5) 60+ や * がこのように動作するように変更せよ.