プログラムdeタマゴ

nodamushiの著作物は、文章、画像、プログラムにかかわらず全てUnlicenseです

EclipseCDTにC方言を追加するプラグインを作ろう その1

 はい、誰の得になるのかわからない超ニッチシリーズが再び始まりました。

 組み込みでは、GNU CやらVisual C Compilerなんかは使えず、独特なドマイナーコンパイラを使わざるを得ないことがあります。そのドマイナーコンパイラがC標準とかに準拠してくれてたら良いんだけど、コンパイラの拡張構文とか、そもそも準拠してないとかザラにあります。(以降、標準でない拡張や準拠していない構文をまとめてC方言と書きます)

 で、そのC方言を使ってEclipse CDT上で開発すると、こういう風にエラーが出ちゃうんですよね。

f:id:nodamushi:20160725130413p:plain


 今回から解説するのは、こういう風にエディタ上にエラーを出さない様にするために、CDTに方言に対応したパーサーを追加するプラグインをどうやって作るのかって話です。

 この記事ではSDCCに対応するパーサーを作っていきます。
 成果物はこちら。
github.com

概要

 手順は大きく以下の3つになります。

  1. C方言のパーサー(構文解析器)を作る
  2. 構文解析結果をIASTNodeに変換する
  3. パーサーを提供するILanguageを作る


 少しこの手の話に詳しい方なら、「字句解析器」は作らないのか?と思われるかもしれません。ここでは説明を抜きして結論だけ言うと、字句解析器は作りません。CDTが提供するものを使います。

 構文解析器は0から全て自分が作ってもかまいませんが、C言語の仕様を全て理解し、全ての構文を実装するのはなかなか大変です。できることなら方言で拡張された構文だけを実装することで済ましたいものです。というわけで、org.eclipse.cdt.core.dom.lrparserプラグインで提供されるC99パーサーを拡張することでC方言パーサーを新たに作りたいと思います。


環境を整える

 まずはプラグイン開発に必要なものについて。以下の三つが必要です。

  1. Eclipse CDT、C99 LR Parser
  2. Eclipse CDTのソースコード
  3. LALR Parser Generator

 

Eclipse CDT,C99 LR Parser

 Eclipse CDTの開発をするので当然CDTが必要です。すでにCDTを導入済みという人も、オプションであるC99 LR Parserが入ってない可能性があるので、確認をしてください。
f:id:nodamushi:20160728174345p:plain


Eclipse CDTのソースコード

 org.eclipse.cdt.core.dom.lrparserのgrammarをベースにしますので、これをダウンロードしてきます。最初はorg.eclipse.cdt.core.dom.lrparser.sourceプラグインに入ってるかなとか思ったんですけど、肝心のgrammar部分は入っていませんでした。なので、Eclipse CDTのソースコード丸ごと全部ダウンロードしてください。
 私がやったときは、Eclipse CDT/gitのWikiに書いてあるgit://git.eclipse.org:29418/cdt/org.eclipse.cdt.gitからはどうもダウンロードできなかったので、git://git.eclipse.org/gitroot/cdt/org.eclipse.cdt.gitか、GithubのEclipse CDTからcloneしてきてください。

LALR Parser Generator

 次にLALR Parser Generatorをダウンロードしてください。Windowsの人がここからダウンロードすると、2016年現在「lpg-win32_x86_2.0.20」というなぜか拡張子がないものがダウンロードされますので、ファイル名の拡張子として「.exe」をつけてください
 ていうか、このLALR Parser Generatorとか何年も更新されてないし、この元プロジェクトのEclipse IMCってのはもう終わってるみたいだし、大丈夫なのか?って感じですが、少なくともEclipse Neonでは動いたのでまだしばらくは大丈夫そうです。



プラグインの依存関係の設定

 さて、ついにいよいよプラグイン作成に入っていきます。

 Eclipseのプラグインを作る際には、とりあえず、まずはplugin.xmlの依存関係の設定をしてしまいましょう。最低限必要なのは以下の三つ

  1. org.eclipse.core.runtime
  2. org.eclipse.cdt.core
  3. org.eclipse.cdt.core.lrparser

f:id:nodamushi:20160728174346p:plain


UPCのgrammarディレクトリをコピー

 パーサーを作っていきたいとおもいますが、何も例がないところから作っていくのもしんどいです。というわけで、UPC Parserのgrammarを例として拝借します。このUPCもC99をベースに拡張して定義されています。
 cloneしたCDTのディレクトリのトップからupc/org.eclipse.cdt.core.parser.upc/grammarディレクトリをコピーしてきてください。

 grammarディレクトリの中はこうなっています。

  • build.xml: antファイル
  • parserBuild.properties: antの設定ファイル
  • upc: grammarファイルが入ったディレクトリ
    • UPCExpressionParser.g
    • UPCGrammarExtensions.g: 主に編集することになるファイル
    • UPCNoCastExpressionParser.g
    • UPCParser.g
    • UPCSizeofExpressionParser.g

 なお、名前がUPCのままではあれなので、私はSDCCに名称変更しました。

f:id:nodamushi:20160728174347p:plain

parserBuild.propertiesの修正

 parserBuild.propertiesにはLALR Parser Generatorの実行ファイルとテンプレートとなるlrparserのgrammarロケーション設定が3つ書かれています。自分が保存したフォルダと合うように修正してください。



build.xmlの修正

 まずは、build.xmlにあるupc,UPCの文字列を、今から作ろうとしている言語名に変えておいてください。生成されるクラス名にも対応してくるので、きちんと直しておいた方が良いと思います。私の場合は、upcはsdccに、UPCはSDCCに置換しました。

 次に以下の部分を自分が保存したフォルダの場所になるように修正してください。

<import file="../../org.eclipse.cdt.core.lrparser/grammar/generate.xml" />

 また、以下の一文を、先(↑)の修正した場所の前に挿入してください。(引数で渡すのが面倒くさいので、固定で設定ファイルを読ませるようにしただけです)

<property file="parserBuild.properties" />

 次に、次のパラメータを、これから作ろうとするパーサーのパッケージ階層と合うように修正してください。たとえば、私の場合はパッケージをnodamushi.internal.cdt.parser.sdccとしました。

<property name="upc_location" value="../src/org/eclipse/cdt/internal/core/dom/parser/upc" />


 で、locationとか設定したんだからここに結果を出力してくれるかと思ってたのですが、なぜかupcディレクトリにjavaファイルが出力されてしまいます。これを毎回手で移動するのが面倒くさいので、次の移動処理をtarget upcに追加しておいてください。これの後ろについでにclean_l_filesのタスクを呼び出す処理を追加しても良いかも。(中間生成物っぽいlファイルというのは、upc_locationに出力されるんだな、これが)

<move todir ="${upc_location}">
  <fileset dir="./upc"> <!-- ディレクトリ名はそれぞれの環境に合わせてください -->
    <include name="**/*.java"/>
  </fileset>
</move>

 SDCC用に作ったnodamushiの修正ファイルは、こちらを参照してください。build.xml




---Parser.gの修正

 続いては、upcディレクトリ(私の場合はsdccディレクトリ)のファイルを修正します。

 まずは、全てのファイル名の接頭辞UPCを、build.xmlでUPCを置換した文字列に変更します。(私の場合はSDCC)

 UPCGrammarExtensions.g(私の場合はSDCCGrammarExtensions.g)を除く以下のファイルの修正をします。

  • UPCExpressionParser.g
  • UPCNoCastExpressionParser.g
  • UPCParser.g
  • UPCSizeofExpressionParser.g

 これら全ファイルについて、の出力パッケージ名と、UPCGrammarExtensions.gのファイルをインクルードしている部分を修正します。「$Import C99~~~.g $End」となっているところは変更しないでください。

%options package=org.eclipse.cdt.internal.core.dom.parser.upc

↓ 修正後(build.xmlで指定したパッケージ階層と矛盾がないように)

%options package=nodamushi.internal.cdt.parser.sdcc
$Import
    UPCGrammarExtensions.g
$End

↓ 修正後(SDCCの部分は適宜変更してください)

$Import
    SDCCGrammarExtensions.g
$End


なお、UPCSizeofExpressionParser.gの$Importは以下のようになっていますが、$DropRulesの部分は消してください。拡張構文にsizeofに関する内容がある場合は、このように規則を削除するようです。SDCCにはこのような拡張構文はないので、詳しいことは調べていません。。。

$Import
    UPCGrammarExtensions.g
$DropRules

unary_expression
    ::= 'upc_localsizeof' '(' type_id ')'
      | 'upc_blocksizeof' '(' type_id ')'
      | 'upc_elemsizeof'  '(' type_id ')'
$End

↓ 修正後(SDCCの部分は適宜変更してください)

$Import
    SDCCGrammarExtensions.g
$End

GrammarExtensions.gの編集

 SDCCGrammarExtensions.g(UPCGrammarExtensions.g)の中身を編集していきます。ここに拡張構文のパーサーを記述していきます。

 GrammarExtendsions.gは$Defineパートと$Globalsパート、$Terminalsパート、$Rulesパートに分かれています。

  • $Define : テンプレートが使う変数の定義
  • $Globals : Javaのimport文を定義
  • $Terminals : 終端記号を定義
  • $Rules : 構文解析規則を記述

$Define

 $Defineパートでは、PraserActionクラス、ASTNodeFactoryクラス、SecondaryParserFactoryを指定します。といっても、今の段階では何も作っていないので、とりあえず、UPCの文字列を作る言語の名前に変えておけば良いと思います。実態は後で作ります。

$build_action_class /. SDCCParserAction ./
$node_factory_create_expression /. new SDCCASTNodeFactory() ./
$parser_factory_create_expression /. SDCCSecondaryParserFactory.getDefault() ./

 あと、2016年7月現在、このままの$Defineではコンパイルが通りません。
 とりあえず、コンパイルを通すという目的だけで、私は$Buildと$EndBuildという変数を上書き定義しておきました。正しいかどうかはともかく、こうしておけば、ひとまず通ります。

$Define
  $build_action_class /. SDCCParserAction ./
  $node_factory_create_expression /. new SDCCASTNodeFactory() ./
  $parser_factory_create_expression /. SDCCSecondaryParserFactory.getDefault() ./
  $Build /. action. ./
  $EndBuild /. ./
$End

 


$Globals

 次に$Globalsですが、importに何が必要かとか、今の段階ではわかりません。ひとまず空っぽにして、放っておききましょう。実際にSDCCParserActionなどを実装してから、最後に必要なものを足せばOKです。



$Terminals

 LALR Parser GeneratorではBNFを用いて構文を記述していきます。
 $Terminalsには、追加する終端記号を定義します。

 え?BNFって何?終端記号って何それおいしいの?って人は詳しくは、このページ(BNFおよび拡張BNF)Wikipediaなどを読んでください。あ、あと、Hatada's Home Page様の構文解析あたりも、とても有用な情報だと思います。

 ここではザックリと、構文に追加したいキーワードとだけ言っておきます。SDCCでは以下のものを定義しました。

$Terminals
  __data  __near  __xdata  __far  __idata  __pdata
  __code  __bit  __sfr  __sfr16  __sfr32  __sbit
  __at  __banked  __interrupt  __using  __reentrant
  __critical  __naked  __wparam  __shadowregs
  __preserves__regs  __asm  __endasm
$End

 なお、本当のキーワードと同じ名前でなくてかまいません。構文上、「--」はコメントと見なされてしまいますし、Javaの変数名に使えない文字列を使うことはできません。そういった場合は、何か別のわかりやすい名前をつけましょう。(C99のgrammarでは「...」はDotDotDotとか名前がついています。)



$Rulesの構文

 BNFに従って追加構文を記述していきます。BNFに追加されている特殊な構文について説明します。
 なお、BNFなにそれおいしいの?って人はこのページ(BNFおよび拡張BNF)Wikipediaなどを読んでください。


 1. rule ::= A | B | C というのは以下のように、ばらして書くことができるっぽいです。

rule ::= A
rule ::= B
rule ::= C

 2. 文法にマッチしたときに行うJavaの処理を、/. ./で括って定義することができます。(定義しなくても良いです)

rule 
  ::= 'HOGEHOGE'
   /.  System.out.println("終端記号HOGEHOGEにマッチした");  ./
    | 'MOGEMOGE'
   /.  System.out.println("終端記号MOGEMOGEにマッチした");  ./
    | rule1 rule2
   /. System.out.println("rule1の処理をした後、rule2の処理をし、この文字列がでる"); ./

 3. 文法的には単にrule ::= rule1 rule2なんだけど、rule1とrule2の間に特別にJavaの処理を挟みたい場合があります。そういう場合には、空にマッチする$emptyを使うことができます。このテクニックは<openscope-ast>で使います。

rule ::= rule1 rule1_2 rule2

rule1_2 ::= $empty
         /. System.out.println("1と2の狭間");

 

$Rulesに追加規則を定義する

 さて、ついにBNFで文法を追加していきましょう。Javaでの処理定義は、ひとまずBNFを作った後に考えれば良いです。

 追加する規則は、文法のルートとなる規則からたどれる必要があります。まぁ、基本的にはC99Grammar.gに定義されている規則の、どれかに追加すれば大丈夫です。

 じゃぁ、C99Grammar.gで定義されている規則は何があって、どこに追加すれば良いのか………それは………根性でC99Grammar.gを解析する以外ありませぇえん!!!!( ゚д゚)

 ま、まぁ、たぶん大丈夫ですよ。0からC言語の構文全部BNFで書き上げるよりは、ずっと解析するだけの方が簡単ですよ。実際、数時間でなんとか私はなったんですから、大丈夫です、きっと。UPCの例や私の作ったSDCCの例を見ながら、なんとなくできますよ。たぶん。

 後は試行錯誤です。

 一応、私がSDCCの拡張構文を追加するときに使った規則は以下です。

  • type_qualifier : 型の修飾関連の規則
  • simple_type_sqecifier_token : intやdoubleといった組み込み型の規則
  • function_direct_declarator : 関数名と引数の規則
  • statememt : 基本的にはブロックとかスコープが絡むようなものっぽい

 あと、C99で何が終端記号として宣言されているかは確認しておいた方が良いでしょう。私は、integer(整数)とidentifierの終端記号を利用しています。


 SDCCのために追加したBNF。(Javaの処理は省く)

-----------------------------------------------------------------------------------
-- Declarations
-----------------------------------------------------------------------------------
type_qualifier
    ::= address_space_name_qualifier
      | define_address
      | sdcc_type_qualifier


-- 以下二つを分けてる理由は特にない
address_space_name_qualifier
    ::= '__data'
      | '__near'
      | '__xdata'
      | '__far'
      | '__idata'
      | '__pdata'
      | '__code'
      | '__sfr'
      | '__sfr16'
      | '__sfr32'

sdcc_type_qualifier
   ::= '__banked'

define_address
    ::= '__at' '(' absolute_address ')'
      | '__at' absolute_address

absolute_address
    ::= 'integer'



simple_type_specifier_token
    ::= '__sbit'
      | '__bit'

-----------------------------------------------------------------------------------
-- Function
-----------------------------------------------------------------------------------

-- ↓C99Grammar.gからfunction_direct_declaratorをコピー
original_function_direct_declarator
    ::= basic_direct_declarator '(' <openscope-ast> parameter_type_list ')'
          /. $Build  consumeDirectDeclaratorFunctionDeclarator(true, true);  $EndBuild ./
      | basic_direct_declarator '(' ')'
          /. $Build  consumeDirectDeclaratorFunctionDeclarator(true, false);  $EndBuild ./


function_direct_declarator
  ::= original_function_direct_declarator sdcc_function_attributes


sdcc_function_attributes
 ::= sdcc_function_attribute
    | sdcc_function_attributes sdcc_function_attribute
 
sdcc_function_attribute
  ::= address_function_attribute
    | '__critical'
    | '__reentrant'
    | '__naked'
    | '__shadowregs'
    | '__wparam'
    | preserves_regs_attribute


address_function_attribute
  ::= '__interrupt' '(' absolute_address ')'
    | '__interrupt' absolute_address
    | '__using' '(' absolute_address ')'
    | '__using' absolute_address
    | '__interrupt'
    | '__using'

preserves_regs_attribute
    ::= '__preserves__regs' '(' ')'
      | '__preserves__regs' '(' preserves_regs_args ')'

preserves_regs_args 
    ::= preserves_regs_arg
    | preserves_regs_args ',' preserves_regs_arg

preserves_regs_arg
    ::= 'identifier'

-----------------------------------------------------------------------------------
-- Statements
-----------------------------------------------------------------------------------

statement
     ::= critical_statement
       | oldasm_satement

critical_statement
   ::= '__critical' compound_statement

oldasm_satement
   ::= '__asm' oldasm_contents '__endasm' ';'
     | '__asm' '__endasm' ';'


oldasm_contents
   ::= oldasm_content
     | oldasm_contents oldasm_content

oldasm_content
   ::= oldasm_item

-- __endasm以外全部
oldasm_item  ;;= 省略




 次回はJavaで何をどう処理するのかの部分を実装していきます。