LoginSignup
16
16

More than 5 years have passed since last update.

Erlangでオレオレ言語の作り方

Last updated at Posted at 2016-10-13

言語実装 Advent Calendar 2015の22日目として書かれました。
ErlangでBEAMで動くコンパイラする作成を説明します。

ulangというサンプルプログラムを作ったのでこれを元に説明します。
https://github.com/oskimura/ulang.git

主な流れとしては字句解析器、構文解析器を行って中間表現に変換しcompileモジュールを使ってBEAMで実行可能なバイナリを作成するという流れです。
今回はulang.xrlで字句解析を行い、ulang_yecc.yrlで構文解析及び中間表現の出力、compiler.erlでバイナリ出力するようにつくりました。

字句解析器


Erlangにはleexという字句解析器があります。
http://erlang.org/doc/man/leex.html
類似のツールを使ったことがある人なら分かるでしょうけど、Definitions、Rules、Erlang codeに分かれています。
Definitionsはruleで仕様される正規表現の定義を行います。
今回はこの箇所に該当します。

INT        = [0-9]+
ATOM       = :[a-z_]+
VAR        = [a-z0-9_]+
CHAR       = [a-z0-9_]
WHITESPACE = [\s\t\n\r]

Rulesは生成するトークンを記述します。

Erlang codeはRulesで仕様されるErlangのコードを記述します。
今回はこの部分に該当します。

module  : {token,{module,TokenLine}}.

のように

マッチする字句 : {生成するトークン}

という風に書きます。

{token,{いろいろ,行数}}.

という風になっているようです。
Definitionsで定義された定義を仕様するには
という風にINTなら{INT}と{}で囲む必要があるようです。

今回の場合はここの
to_atom関数に該当します。
この様にRulesで使用することができます。

今回はこのように作成しました
https://github.com/oskimura/ulang/blob/master/ulang/src/ulang.xrl

構文解析器


同じようにErlangにはyeccという構文解析器があります。
http://erlang.org/doc/man/yecc.html
yeccはErlangのコンパイラコンパイラでBNFで文法を記述できます。
ちなみにErlangのパーサもyeccによって書かれています

yeccはNonterminals、Terminals、規則部、Erlang codeに別れています。
Nonterminalsは非終端記号の集合の宣言を行います

Terminalsは終端記号の宣言を行います

規則部はRootSymbolを開始記号とする簡約規則を記述します。RootSymbolがないとエラーになります。
https://github.com/oskimura/ulang/blob/master/ulang/src/ulang_yecc.yrl#L6

規則部は
https://github.com/oskimura/ulang/blob/master/ulang/src/ulang_yecc.yrl#L9-L13

program ->
    module_exp: [ '$1' ].

program ->
    module_exp exps: [ '$1' | '$2' ].

のように
非終端記号 ->  ルール : アクション
という風にかきます。'$1'は引数です。基本的にはlex/flexと一緒です。非終端記号に複数のルールがある場合は上記のように複数書きます。

今回作成した規則部
今回作成するyeccはアクションに作成する中間表現を記述します。中間表現の詳細は後述します。

Erlang codeは
規則部で使用するErlangコードを記述します。leexと同じです。
https://github.com/oskimura/ulang/blob/master/ulang/src/ulang_yecc.yrl#L163-L174

今回作成したたyeccの全体はこちらです。
https://github.com/oskimura/ulang/blob/master/ulang/src/ulang_yecc.yrl

中間表現

erl_syntaxというドキュメントに中間表現のシンタックスツリーを組み立てるためのAPI書いてありますが、
http://erlang.org/doc/man/erl_syntax.html
今回コレは使用せずに自分で組み立てる事にします。
このモジュールでErlangプログラムファイルの中間表現を取得できます。
今回はコレを使って中間表現を解析して中間表現を組み立てます。

compileモジュール

生成された中間表現をコンパイルしてバイナリを出力するのはcompileモジュールでおこないます。 
http://www.erlang.org/doc/man/compile.html

今回はnoenv_forms関数を使用して次のように書きました

compile:noenv_forms(Spec,[return]) of
 {ok,Module,Binary,Wornings} ->
end

compile:noenv_formsの第一引数はバイナリ変換したい中間表現、第二引数はオプションです。
コンパイルが成功した場合は{ok,Module,Binary,Wornings}というタプルが返されます。
Moduleはモジュール名、Binaryは変換バイナリ、Worningはコンパイル時の警告です。

中間表現について


中間表現は基本的にタプルで表現され、{タグ名,ソース行数,引数1,引数2,...}といった形になっています。
このタグのリストが中間表現です。compileモジュールの関数に渡すことでバイナリへとコンパイルされます。
サンプルプログラムを例に説明します。
Eralangの文法にかんしてはココを参考にしてください。
では中間表現の説明をしていきます。

module宣言

-module(sample).

というモジュール宣言は

{attribute,1,module,sample},

となります

module := {attribute,Line,module,ModuleName},

Lineは行数、ModuleNameはモジュール名となります。
attributeというタグは後述するexportなどmoduleの箇所を変えて使われます。
ModuleNameはモジュールの名前です今回はsampleです。

export宣言

-export([func1/0]).

というexport宣言は

{attribute,2,export,[{func1,0}]},

と変換されます。

export := {attribute,Line,export,[{f1,Arg},...]},

exportの後に関数のリストが必要です。{変数名,関数の数}のリストという形で表現されます。

関数宣言

func1 () ->
    ok.

という関数宣言は

{function,3,func1,0,[{clause,3,[],[],[{atom,4,ok}]}]}

と変換されます。

function := {function, Line, FunctionName, ArgNumber [Clause1,...]}

Lineは行数、FunctionNameは関数名、ArgNumberは引数の数、Clauseのリストは関数の本体です。
なぜリストなのかというと、次のような場合

func2([]) ->
    ok;
func2(X) ->
    ok.

引数のマッチングによって複数の本体があるからです。
ちなみにこのClauseはcase hoge of~形式のパタンマッチの中間表現と共通の形式です。

clause := {clause,Line,[Args],[TestFuncs],[Bodys]}

Lineは行数、Argsのリストは仮引数、TestFuncのリストはガード節、Bodysのリストは関数本体です。
ガード節は

func3(X)  when is_atom(X)  ->
    X.

のwhen以降で引数のチェックを行う箇所です。
上記のような関数の場合

{function,10,func3,1,[{clause,10,[{var,10,'X'}],[[{call,10,{atom,10,is_atom},[{var,10,'X'}]}]],[{var,11,'X'}]}]}

と変換されます。

文字列

string_fun() ->
    "abc".

{function,15,string_fun,0,[{clause,15,[],[],[{string,16,"abc"}]}]}

と変換されます。

{string,Line,Str}

Strは文字列です

文字

char_fun() ->
    $a.

{function,18,char_fun,0,[{clause,18,[],[],[{char,19,97}]}]}

と変換されます。

{char,Line,Char}

Charは文字のASCIIコードです

整数

integer_fun() ->
    1.

{function,21,integer_fun,0,[{clause,21,[],[],[{integer,22,1}]}]

と変換されます。

{integer,Line,Num}

Numは数字です

演算子

op_fun() ->
    1+1.

{function,24,op_fun,0,[{clause,24,[],[],[{op,25,'+',{integer,25,1},{integer,25,1}}]}]}

と変換されます。

{op,Line, Op,LVal,RVal}

Opは演算子です。LValとRValはそれぞれ演算子の左辺と右辺です。

関数呼び出し

callで呼ぶことができる関数は同じモジュール内の関数に限られる。他のモジュールで宣言してある関数は 後述するremote callで呼び出す必要があります。

call_fun() ->
    func1().

という関数は

{function,13,call_fun,0,[{clause,13,[],[],[{call,14,{atom,14,func1},[]}]}]},

と変換されます。

call := {call,Line,{atom,Line,FunctionName},Args},
var := {var,Line,VarNme}

Lineは行数、FunctionNameは関数名、Argsは引数のリストです。

関数呼び出し

他のモジュールで宣言された関数はremote callで呼び出す必要があります。

remote_call_fun() ->
    io:format("test").

という関数は

{function,16,remote_call_fun,0,[{clause,16,[],[],[{call,17,{remote,17,{atom,17,io},{atom,17,format}},[{string,17,"test"}]}]}]}

と変換されます。

remotecall :={call,Line,{remote,Line,{atom,Line,ModuleName},{atom,Line,FunctionName}},Args}
Args := {var,Line,VarNme}

remoteが入る以外はcallと同じです。

パタンマッチ

パタンマッチです

match_fun(X) ->
    case X of
        a ->
            a;
        b ->
            b

という関数が

{function,19,match_fun,1,[{clause,19,[{var,19,'X'}],[],[{case,20,{var,20,'X'},[{clause,21,[{atom,21,a}],[],[{atom,22,a}]},{clause,23,[{atom,23,b}],[],[{atom,24,b}]}]}]}]}

と変換されます。

case := {'case', Line, MatchExp, [Clause1,...]}
matchexp := {clause, Line, Match, Test, Bodys}

MatchExpは上のソースでいうcaseとofの間の式にあたりますClauseの

代入

単一代入です。

bind_fun() ->
    X = 1.

{function,29,bind_fun,0,[{clause,29,[],[],[{match,30,{var,30,'X'},{integer,30,1}}]}]}

と変換されます。

{match,Line,Var,Val}

VarにValを代入します。

{var,Line,Var}

は変数です。Varは変数名です。

{integer,Line,Val}

は整数です。Valは設定する整数です。

リスト

list_fun() ->
    [1,2,3,4].

という関数は

{function,27,list_fun,0,[{clause,27,[],[],[{cons,28,{integer,28,1},{cons,28,{integer,28,2},{cons,28,{integer,28,3},{cons,28,{integer,28,4},{nil,28}}}}}]}]}

と変換されます。

{cons, Line, {Elent1,{Element2, Line,... {nil,Line}}

Element1,Element2はリストの要素です。consは入れ子構造になってます。

実演 fibをコンパイルしてErlngから呼び出してみましょう

output.gif

最後に

上記の情報があればBEAMで動く処理系が作れるとおもいます。中間表現の他の文法等についてはいろいろ試してみよう

16
16
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
16