最近の更新 (Recent Changes)

2014-01-01
2013-01-04
2012-12-22
2012-12-15
2012-12-09

Wikiガイド(Guide)

サイドバー (Side Bar)


← 前のページに戻る

2. プログラミング言語PL/0の実装

2.1 如何にコンパイラをつくり、実行できるバイナリ形式に変換するか

2.1.1 コンパイラの構造

コンパイラは、一般的に字句解析、構文解析、意味解析、コード最適化、およびコード生成で構成されます。

これは基本的な構成ですが必ずしてもこの構成にする必要はなく、コンパイラによっては、途中で中間コードに変換したり、最適化をべつの段階で複数実行したりすることも可能です。 そのあたりの工夫は、コンパイラの作者の腕の見せ所でもあります。


compile1.jpg

字句解析

字句解析は、プログラムのソースを読み込み、それを最小限の要素(トークン)に分解します。 先に示したPL/0の構文でいえばトークンは、非終端記号"procedure"や"var"のような定型の文字列(キーワード)やカンマやピリオド、セミコロンのような記号、および識別子ID(変数名、定数名、手続き名)、数値NUM、文字列STRINGSのような可変な文字列に相当します。


トークンの要素

  procedure, const, var, begin, end, if, while, printなどのキーワード

  ",", "=", ";" などの記号

  変数名、定数名、手続き名などの識別子

  1, 100, 283などの数値

  "VVG", "index", "num"などの文字列

字句解析によって、トークンごとに判別することが容易になり、次に示す構文解析での構文の解釈を支援します。

構文解析

構文解析は、字句解析で得られたトークンの組み合わせをまとめて、正しい構文であるかどうかを判定します。

構文解析により、定型のトークンや記号と、変数や数字のような可変なトークンが構文の中でどのような役割を果たすのか解析することにより明らかになります。


例えば、次の構文は<program>は<block>と"."で構成されることを示す。

<program>       <block> "." ;

さらに<block>の構文は~というように、順に入力されたソース・プログラムを分解していき最終的にはトークンの集合になるように解析する。

最終的に入力されたソース・プログラムがすべて非終端記号を含まない終端記号のトークンに分解されれば構文解析は完了します。 その状態は、ソース・プログラムがすべてコンパイラ言語の構文の文法に従い間違いのないことを表します。

ここでどうしても構文解析に従わないトークンの並びが現れた場合には、それはコンパイルエラーとしコンパイル処理を中断することになります。

デカルト言語の場合は、字句解析と構文解析を合わせた機能を元々の言語機能として持ちます。 そのため、構文解析プログラムをキーワードとID, NUMなどの組み込みのトークン機能で実現すれば、それで字句解析と構文解析を行うプログラムとなります。

意味解析

意味解析では、構文解析の結果を受けてプログラムソースの中のそれぞれの要素の意味について解析します。

ここでは、構文解析では単に識別子のトークンとして得られたIDを変数名、定数名、手続き名なのか判別し、それぞれの持つ型について正しいか判断します。型が誤まっていた場合には、コンパイルエラーとしコンパイル処理を中断することになります。

また、構文の中の文の並びの要素で、引数などの可変の要素が正しい識別子であるか、正しい型であるかを判定します。 例えば、代入文の中では定義されている変数しか使えません。定義されていない変数が書かれていた場合にはコンパイルエラーとします。


var a;
begin
   a := 0;
   b := a + 1;
end.

構文的には正しいが、変数bは未定義なのでエラー。

具体的には意味解析では、変数や手続き名は定義されると管理する表に型などの情報が登録され、プログラムの中の使用される場面ごとに管理表と比較することにより正しい使い方になっているかを判定します。

このように構文解析では判定できなかった最終的なチェックを意味解析で行います。

コード最適化

コード最適化では、意味解析まで終了した結果の中に無駄な処理がないか調べ、効率化できるところは改善します。 この結果として、コンパイル結果のプログラムの実行速度や使用メモリ量の節約が行われます。

例えば、次のような処理です。


- 無駄なループ処理を省略
- 使用されない変数を削除
- 常に同じ結果になる演算式の定数への置き換え

コード最適化については、現在の一般的なコンパイラではさまざまな高度な方法がとられ極めて最適化されたコードを出力することができるようになりました。

コード生成

コード生成では、コード最適化まで行った結果を基にコンピュータ上で実行できるコードを生成します。

通常はアセンブラコードをここで出力します。バイナリコードを直接出力したり、JAVAのようにVMコードを出力する場合もあります。

この処理により、プログラムソースとして記述されたプログラムは実際に実行することが可能なコードに変換されます。

2.1.2 PL/0トランスレータ

前項で説明したコード生成部ですが、通常はアセンブラコードを出力します。 それにより、そのコンピュータのアーキテクチャで直接実行できるバイナリコードを得ることになります。 しかし、それ故にその部分はアーキテクチャ毎に異なります。

最近のCコンパイラなどでは、複数のアーキテクチャのコードを吐き出すことができるものもありますが、新たにコンパイラを作成しようとする場合には通常はこの部分も自分で作るしかありません。

ここで、新規にコンパイラを作りたい、しかし、コード生成部のアーキテクチャ依存のアセンブラ出力は面倒だ、と考えたとしましょう。

どうするか?

そうだ、アセンブラではなくて、コンパイル結果としてC言語のソースを出力したらどうだろう。 C言語のソースは細かなことを言わなければ機種依存しないし、GNUの出しているGCCを使えば様々なアーキテクチャをサポートするし。 CYGWINやMINGWならば、Windows上でも使えるし。

というわけで、ここでは、C言語ソースを出力するPL/0コンパイラ(トランスレータ)を作ろうと思います。

さらに最近のCコンパイラならば、コード最適化もいろいろと工夫して実装しています。 ついでにコード最適化についても、Cコンパイラに任せてしまいましょう。


compile2.jpg

さらに、意味解析部分についても、未定義変数や未定義手続きの検出などは、Cコンパイラに任せてしまうと楽ができます。

プロトタイプ的に実装するには、これでも充分ではないでしょうか。


compile3.jpg

このようなトランスレータならば、独自の新言語を作成するときにもとりあえず実装するのに使えるでしょう。 実際に使ってみて問題なければ、本格コンパイラとして各フェーズを再度作り直してやればよいのですから。

2.2 コンパイラのソース

PL/0コンパイラのソースを以下に示します。

前の章で示した構文定義に対して、print文やprintf文が追加されていることがわかるでしょうか。 構文の解析が終ったところで片っ端からC言語のソースに変換しているのです。


// Compiler of PL/0 by descartes (translator)

<program>                       <print "#include <stdio.h>">
                                <print "#include <stdlib.h>">
                                <print>
                                <print "void main() ">
                                <print "{">
                <block>         <emsg "'.' is missing.">
                "."             <print "}">
                ;
<block>         [ "const"       <emsg "constant name.">
                                <printf "const int ">
                  <ident>       <emsg "constant definition.">
                  "="           <printf " = ">
                  <number>
                  {","          <printf ", ">
                   <ident>      <emsg "constant definition.">
                   "="          <printf " = ">
                   <number>
                   }            <emsg "';' is missing.">
                   ";"          <print ";">
                ]
                [ "var"         <emsg "variable name.">
                                <printf "int ">
                  <ident>       <emsg "variable definition.">
                  {","          <printf ", ">
                   <ident>      <emsg "variable definition.">
                   } ";"        <print ";">
                ]
                { "procedure"   <emsg "procedure name.">
                                <printf "void ">
                  <ident>       <emsg "procedure definition.">
                                <print "()">
                  ";"
                                <print "{">
                  <block> ";"   <print "}">
                }
                <statement>
                ;
<statement>     <SKIPSPACE> ::sys <line #Line> <setVar Line #Line>
                [
                  <ident> ":="  <emsg "expression.">
                                <printf " = ">
                  <expression>  <print ";">
                | "call"        <emsg "procedure.">
                  <ident> <print "();">
                | "begin"       <print "{">
                     <statement> {";" <statement>}
                  "end"         <print "}">
                | "if"          <emsg "if sentence.">
                   <printf "if (">
                   <condition>
                   "then"       <print ") ">
                   <statement>
                | "while"       <emsg "while sentence.">
                                <printf "while (">
                  <condition>
                  "do"          <print ")">
                  <statement>
                |
                  "print"       <emsg "print sentence. ">
                   [
                    (
                      <ident #id>       <print 'printf("%d ", ' #id ');'>
                    | <number #s #n>    <printf 'printf("%s%d ", "' #s'" ,' #n ');'><print>
                    | <strings #str>    <printf 'printf("%s ", "'#str'" );'><print>
                    )
                   { ","
                    (
                      <ident #id2>      <print 'printf("%d ", ' #id2 ');'>
                    | <number #s2 #n2>  <printf 'printf("%s%d ", "' #s2'" ,' #n2 ');'><print>
                    | <strings #str2>   <printf 'printf("%s ", "'#str2'" );'><print>
                    )
                   }
                   ]
                                        <print 'printf("\n");'>
                ];
<condition>     "odd"           <emsg "odd syntax.">
                                <printf "((">
                  <expression>  <printf ") & 1)">
                |
                <expression>
                ("="            <printf " == ">
                |"#"            <printf " != ">
                |"<="           <printf " <= ">
                |"<"            <printf " < ">
                |">="           <printf " >= ">
                |">"            <printf " > ">
                )
                <expression> ;
<expression>    [ "+"           <printf "+">
                 |"-"           <printf "-">
                ] <term> {
                        ("+"    <printf "+">
                        |"-"    <printf "-">
                        ) <term>};
<term>          <factor> {
                ("*"            <printf "*">
                |"/"            <printf "/">
                ) <factor>};
<factor>        <ident> | <number>
                | "("           <printf "(">
                        <expression>
                  ")"           <printf ")">
                ;

<ident #id>     <ID #id>
                <reserved #id>
                ;
<ident>         <ident #id>
                <printf #id>
                ;
<number #sign #n>
                ["+"            <is #sign "">
                |
                 "-"            <is #sign "-">
                |
                                <is #sign "">
                ]
                <NUM #n>
                ;
<number>        <number #sign #n>
                <printf #sign #n>
                ;
<strings #str>  <STRINGS #str>
                ;
<strings>       <strings #str>
                <printf #str>
                ;

<reserved #id>
                <not ::sys <member #id
                  (var procedure begin end if then while do call print odd)>>
        ;

<emsg #x>
                <x <getVar #Line Line>
                   <warn "error : " #Line ":" #x>
                   <exit>>
                ;

<compile>
        ::sys<args #x>
        ::sys<nth #inputfile #x 1>
        ::sys<suffix #outputfile #inputfile c>
        <print inputfile #inputfile>
        <print outputfile #outputfile>
        ::sys<openw #outputfile
                ::sys<openr #inputfile <program>>>
        ;

? <compile>;



元の拡張バッカス構文(EBNF)に対して、3段目のインデントでC言語へのソース出力処理を追加しています。


1段目     2段目         3段目
非終端記号   構文定義  C言語ソースの出力処理

2段目までは、1.4 デカルト言語での構文の記述と同じ記述であります。 3段目に対して新たに今回のトランスレータの処理が追加されたました。

詳細については、「2.4 コンパイラのソースの詳細な説明」で説明します。

2.3 PL/0ソースのコンパイル方法

「2.2 コンパイラのソース」で示したソースを、"pl0.dec"という名前で保存しましょう。

そして次に示すサンプルのPL/0プログラムは、"a.pl0"という名前で保存しておきます。


const x = 2;
var   a;
procedure test;
begin
        a := 1;
        a := a + x
end;

begin
        call test;
        print a
end.

コンパイルを実行してみましょう。


$ descartes pl0.dec a.pl0
inputfile a.pl0
outputfile a.c
result --
        <compile>
-- true

"a.pl0"を引数に指定すると、自動的にa.cというC言語のソースが出力されます。

"a.c"の中を見てみましょう。


#include <stdio.h>
#include <stdlib.h>

void main()
{
const int x = 2;
int a;
void test()
{
{
a = 1;
a = a+x;
}
}
{
test();
printf("%d ",  a );
printf("\n");
}
}

インデントが変ですね。直してみましょう。


#include <stdio.h>
#include <stdlib.h>

void main()
{
    const int x = 2;
    int a;
    void test() {
        {
            a = 1;
            a = a + x;
        }
    }
    {
        test();
        printf("%d ", a);
        printf("\n");
    }
}


どうでしょうか。 正しいC言語のソースに変換されたように見えます。

いや、変だぞ!!という人がいるかもしれません。 よく見てみるとmain関数の中で、test()関数を定義しているのです。 本来のC言語ではこのような関数内関数は定義できないはずです。なぜ?

実はGCCでは、C言語が拡張されていて関数内関数が定義できます。他のC, C++コンパイラではたぶん使えないでしょう。

そこで、今回のコンパイラというかトランスレータではGCCのみを対象とすることにします。

このC言語のソースをGCCを使ってコンパイルして実行してみましょう。


$ gcc a.c -o a.out

$ ./a.out
3

関数内関数の定義にも関わらず何のコンパイルエラーもWarningさえも起きていません。

また、実際には出力されたC言語のソースのインデントが変でもかまいません。いちいち修正する必要なくそのままGCCでコンパイルしてしまえばよいのです。

2.4 コンパイラのソースの詳細な説明

2.4.1 入力ファイルと出力ファイルの決定とコンパイラ本体の呼び出し

まずは、ソース「2.2 コンパイラのソース」の最後の部分に着目してください。


<compile>
        ::sys<args #x>
        ::sys<nth #inputfile #x 1>
        ::sys<suffix #outputfile #inputfile c>
        <print inputfile #inputfile>
        <print outputfile #outputfile>
        ::sys<openw #outputfile
                ::sys<openr #inputfile <program>>>
        ;

? <compile>;

? <compiler>;からすべての処理が始まります。

呼び出された<compiler>の定義ではまず、::sys<args #x>が呼ばれ、プログラムの引数が#xに設定されます。

argsはデカルト言語の組み込み述語であり、プログラムを呼び出したときの引数を設定します。


$ descartes pl0.dec a.pl0

上に示した場合では、#xにリスト(pl0.dec a.pl0)が設定されます。

次に、::sys<nth #inputfile #x 1>で、#xの1番目(リストは0から始まる)であるPL/0サンプルであるa.pl0のソースファイル名が#inputfile変数に設定されます。

::sys<suffix #outputfile #inputfile c>では、#inputfileの拡張子をpl0からc に変換して、#outputfileに設定します。

suffixはデカルト言語の組み込み述語であり、第2引数の拡張子を第3引数に変換して、第1引数に設定します。

これで、入力ファイルa.pl0から、 出力ファイルa.cを設定できました。


::sys<suffix #outputfile #inputfile c>によって、

   a.pl0
    ↓
   a.c

a.pl0がプログラムソースであり、それがa.cにコンパイルされた結果が出力される。

<print inputfile #inputfile>と<print outputfile #outputfile>では、コンパイルの情報として入力ファイルと出力ファイルについて表示しておきます。

さて、ここからは実際のコンパイル処理が始まるので着目してください。

::sys<openw #outputfile ::sys<openr #inputfile <program>>>では、標準出力を#outputfileに設定し、標準入力を#inputfileに設定してから、メインのプログラムである<program>を呼び出します。

ここまでの処理で、入力ファイルと出力ファイルが決定されて、コンパイラの本体である字句解析と構文解析を行う<program>が呼び出されました。


入力ファイル : #inputfile
出力ファイル : #outputfile
字句解析と構文解析 : <program>

2.4.2 program

では、ソースの最初のほうに戻りましょう。

まず、前項で呼び出された <program> の定義です。


<program>                       <print "#include <stdio.h>">
                                <print "#include <stdlib.h>">
                                <print>
                                <print "void main() ">
                                <print "{">
                <block>         <emsg "'.' is missing.">
                "."             <print "}">
                ;


これは、programは、blockと"."で出来ていることを示している構文を処理します。

デカルト言語では、<>で囲まれていない文字列が現れた場合には、標準入力から空白を読み飛ばしたデータと比較して一致した場合にTRUEとなってデータを詠み進めます。"."では、標準入力からピリオドが読み込まれると処理が成功して次に進みます。(ピリオド以外の文字列があると失敗して前の処理に戻ります。前の処理はemsgなので引数のエラーメッセージを表示して終了します。)

インデントの1段目と2段目が構文の定義を示しています。 インデントの3段目が、その構文にソースプログラムが合致したときの処理を表しています。

ここでは、C言語(GCC)のソースを出力するためにprint文が並んでいます。 前の項でオープンされた#outputfileに設定されたファイルにprint文の結果が書き込まれます。

次にprint文の詳細を見ていきましょう。

まず、<program>の後にある実行文は、<program>が呼び出された場合には無条件に出力されます。 可変の要素もなくソースを読む前なので、構文エラーにもならないため、<block>の前までの print文は必ず実行され、出力ファイルに書き込まれます。

その内容は、次のようになります。


#include <stdio.h>
#include <stdlib.h>

void main()
{

C言語では一般的なヘッダの読み込みとmain関数の定義です。 この部分は最初に必要な共通の処理なので必ず出力するようになっています。

次の<block>は、次項から説明しますが<block>構文の解析処理を呼び出します。

その後の、<emsg "'.' is missing.">は、さらに次の行の "." の構文解析に失敗、つまりピリオドがブロックの後に見つからなかったときのエラーメッセージを表示します。

emsg述語は、PL/0のソース内で定義していますが後に説明します。ここでは、この述語が呼ばれた後の処理が失敗するとこの述語の引数のメッセージが表示されて、コンパイル作業が終了することを覚えておいてください。


                <block>         <emsg "'.' is missing.">  ← emsg以降の処理が失敗するとここに戻ってエラー出力
                "."             <print "}">


最後に"."が無事構文解析に成功すると最後の<print "}">がコンパイル結果として出力されます。

<program>の処理では、こんな感じにmainプログラムの大枠が出力されるのです。


#include <stdio.h>
#include <stdlib.h>

void main()
{
  *** ここには、<block>の呼び出し時に出力される処理が入る。 ***

}

こうして、コンパイル結果の出力のC言語ソースが吐き出されていきます。

2.4.3 block

次はblockの構文です。


<block>         [ "const"       <emsg "constant name.">
                                <printf "const int ">
                  <ident>       <emsg "constant definition.">
                  "="           <printf " = ">
                  <number>
                  {","          <printf ", ">
                   <ident>      <emsg "constant definition.">
                   "="          <printf " = ">
                   <number>
                   }            <emsg "';' is missing.">
                   ";"          <print ";">
                ]
                [ "var"         <emsg "variable name.">
                                <printf "int ">
                  <ident>       <emsg "variable definition.">
                  {","          <printf ", ">
                   <ident>      <emsg "variable definition.">
                   } ";"        <print ";">
                ]
                { "procedure"   <emsg "procedure name.">
                                <printf "void ">
                  <ident>       <emsg "procedure definition.">
                                <print "()">
                  ";"
                                <print "{">
                  <block> ";"   <print "}">
                }
                <statement>
                ;


bllockは、定数constの定義、変数varの定義、手続きprocedureの定義と一つのstatementで構成されます。

2.4.3.1 定数const定義

まず定数constの定義ですが、最初ですので詳しく見てみましょう。


定数constの定義

                [ "const"       <emsg "constant name.">
                                <printf "const int ">
                  <ident>       <emsg "constant definition.">
                  "="           <printf " = ">
                  <number>
                  {","          <printf ", ">
                   <ident>      <emsg "constant definition.">
                   "="          <printf " = ">
                   <number>
                   }            <emsg "';' is missing.">
                   ";"          <print ";">
                ]


まず全体が[]で囲まれているので省略可能です。これはEBNFの仕様と同じですね。

次に"const"文字列と標準入力(今はPL/0プログラムの入力ソース)のデータが一致するか判定します。

成功すれば、"const int"と標準出力(今は出力先のC言語ソース)へ出力します。

(失敗すれば、次の変数varの定義へ進みます。)

<emsg "constant name."> は、次の<ident>の処理が失敗した場合に出力されるメッセージを設定しておきます。

<ident>は、定数名かどうかをチェックし、"="、<number>と順にチェックすることにより、"定数名 = 数値"のような値かどうかをチェックして、付属するprintにより、、"定数名 = 数値"と出力します。

次の{}は、0回以上の繰り返しを意味し、","とその後の定数定義があればチェックします。

最後は";"で定数定義の最後にセミコロンがあれば、定数定義は完了です。

例を示すと次のように変換されます。


PL/0ソース

const abc = 1, data = 283;

  ↓

C言語のソース出力

const int abc = 1, data = 283;

結果としてはconstがconst intに変わっただけですが、ちゃんとソースの入力をスキャンした結果なので構文として正しかったことを保障しています。もし構文上の誤りがあれば、エラーメッセージを表示してコンパイル作業は終了します。

2.4.3.2 変数var定義

次は変数varの定義です。


変数varの定義

                [ "var"         <emsg "variable name.">
                                <printf "int ">
                  <ident>       <emsg "variable definition.">
                  {","          <printf ", ">
                   <ident>      <emsg "variable definition.">
                   } ";"        <print ";">
                ]

これは、constの定義とほとんど同じですね。

違いは、初期値がないことで、変数名を, (カンマ)で区切り、最後に; (セミコロン)を置きます。

また、定数ではないので、int型として変数は定義されC言語ソースとして出力されます。

例を示すと次のように変換されます。


PL/0ソース

var vvg, i, j;

  ↓

C言語のソース出力

int vvg, i, j;

2.4.3.3 手続きprocedure定義

次は、手続きprocedureの定義です。


手続きprocedureの定義

                { "procedure"   <emsg "procedure name.">
                                <printf "void ">
                  <ident>       <emsg "procedure definition.">
                                <print "()">
                  ";"
                                <print "{">
                  <block> ";"   <print "}">
                }

まず、「procedure 識別子<ident> ;」と手続き名を定義します。

そして、このprocedureの処理本体としては<block>と最後の; をスキャンします。

この手続きの定義は<block>の中にあったことを思い出してください。そうです、<block>の定義の中の手続きの定義の中で再帰的に<block>を呼び出していることに注意してください。この手続きの中の<block>で再度、定数定義、変数定義、手続き定義を行うこともできるのです。

ここで定義する手続きは、上のprintf述語の処理を見ると判りますが、引数も返り値もないので、「void 手続き名()」という関数定義のC言語として出力されます。

例を示すと次のように変換されます。


PL/0ソース

procedure testsub;
const c = 0;
var   a, b;
begin
  a := c;
  b := a + 1
end;

  ↓

C言語のソース出力

void testsub() {
  const int c = 0;
  int   a, b;
  {
    a = c;
    b = a + 1;
  }
}

begin, endの構文については後に説明しますが、C言語では、{  } に変換されます。

2.4.4 statement

次はstatementの構文です。

statementは、代入、手続き呼び出し、if文、while文、またはprint文のような1行ずつの処理について定義してあります。また、begin ~ endの記述についてもここで定義します。

最初にstatementの処理に以下のようなものがあります。


                 <SKIPSPACE> ::sys <line #Line> <setVar Line #Line>


これは何かと言うと、statementのある行まで空白を組み込み述語SKIPSPACEで飛ばしてから、 組み込み述語lineでその場所までのソース先頭からの行数を調べて、 グローバル変数Lineにその値を設定しています。

このグローバル変数Lineはstatementの処理でエラーが発生した場合にemsg述語で発生場所の行数を表示するために使います。 今までにも出てきたemsg述語でエラーメッセージと同時にエラー行番号を表示します。

2.4.4.1 代入文

最初に代入文のプログラムです。


                  <ident> ":="  <emsg "expression.">
                                <printf " = ">
                  <expression>  <print ";">

ident述語は、変数名と合致するとその変数名を出力します。変数名ではなく(var procedure begin end if then while do call print odd)のようなプログラムのstatementの要素を構成する語句の場合はエラーとなります。ident述語の定義については、後項で説明します。

次の ":=" は代入記号であり、この記号を検出するとprintfで " = " を出力します。

expression述語については、これについても後で説明しますが数式と対応する述語です。数式を構文解析してC言語の数式に変換された値を出力します。他の場所でも<expression>が出てきますが、その場合はそこに数式が書けて、結果が変換されて出力されると考えてください。

数式の最後には ";" を出力します。 PL/0言語では文の区切りに ; が必要なだけで必ずしも行末に付きませんが、C言語に変換する場合には必ず行末に必要であるため忘れずにここで" ; "を出力しておきます。


PL/0言語ソース
    va := a + b * c + 1

  ↓

C言語出力
    va = a + b * c + 1;

2.4.4.2 call手続き呼び出し

次は手続き呼び出しです。


                 "call"        <emsg "procedure.">
                  <ident> <print "();">



callの後に手続き名があると、手続き名はident述語とマッチします。 ident述語はマッチした手続き名を出力し、その後にprintで(); が出力され、C言語では引数も返り値もない関数呼び出しとして出力されます。


PL/0言語ソース
    call testsub

  ↓

C言語出力
    testsub();

ここで、本来は手続き名は手続き定義時にテーブルにあらかじめ登録しておいて、呼び出し時にチェックする等の意味解析時のエラーチェックをすべきです。 しかし、今回のPL/0コンパイラはその役割については全体の構成を簡単にするために敢えてサボります。その処理はGCCコンパイラに丸投げするので、未定義の手続きの呼び出しなどのエラーはGCCでコンパイルするときに出力されます。

2.4.4.3 begin, end

次はbegin, endです。

この構文を使うと本来は1行の処理しか書けない部分に、begin, endで囲むことによって複数行の処理を行うことができます。

if文、while文や、procedureの定義およびメインの処理には、本来1行の処理しか書けません。


if文 : thenの次には1行の処理が書ける

    if a > 1 then
      a := 1

begin, endを使えば複数行書ける。

    if a > 1 then
    begin
       a := 1;
       b := 0
    end

begin, endの中の処理は、複数行ある場合には処理と処理の間に区切りとして ";" を付ける。最後の処理の行には付けない。C言語のように全部の行の最後に ";" が付くわけではないことに注意。

begin, end は、中身の処理をまとめて1行の処理相当に変換しているようなイメージで捉えるとわかりやすいです。


                  <ident> <print "();">
                | "begin"       <print "{">
                     <statement> {";" <statement>}
                  "end"         <print "}">

C言語で言えば、begin, end は、 {, } のカッコに該当します。

そこで上に示すようにbeginが現れたら、{ をコンパイル結果のC言語のソースとして出力します。

statementの中処理であるbegin, endの内部では、再帰的にさらにstatementが呼ばれます。

内部には、statementが一つか、その後に続く ";" で区切られた複数のstatementがあります。statementとstatementの間にのみ ";" が必要なことに注意してください。

そして最後に、endが現れたら、 } を出力します。

2.4.4.4 if文

次は条件判定を行うif文です。


                | "if"          <emsg "if sentence.">
                   <printf "if (">
                   <condition>
                   "then"       <print ") ">
                   <statement>

ifがあると、C言語の出力には if ( と出力します。PL/0言語では条件を各部分は()で括らなくても良いのですがC言語では必要なため、このようになっています。

<condition>は、後で説明しますが条件式、例えば a > 1 のような比較式を書く処理です。

then は、C言語の出力では ) で条件式の終わりを出力します。

最後に<statement>で、1行の実行処理ができるのですが、前項に示したようにbegin, endを使えば複数の実行処理も書けます。


PL/0言語ソース
    if a > 1 then
      a := 1

  ↓

C言語出力
    if (a > 1)
      a = 1;


さて、もともとのPL/0言語にもif文にはelseがありません。おそらく教育的配慮で簡単になっているのでしょう。 しかし、デカルト言語では、例えば次のようにすればelse文も簡単に拡張できます。


                | "if"          <emsg "if sentence.">
                   <printf "if (">
                   <condition>
                   "then"       <print ") ">
                   <statement>
                   [ "else"       <print "else">
                     <statement> ]


"else"とその出力と<statement>が増えただけですね。 さらに、else節はいつもあるわけではなくthen節だけの場合もあるので[, ] で囲んでおき省略可能としておかなければなりません。

2.4.4.5 while文

次は、条件が成立している間は繰り返し処理を行うwhile文です。


                | "while"       <emsg "while sentence.">
                                <printf "while (">
                  <condition>
                  "do"          <print ")">
                  <statement>


これもif文と同じような変換処理をします。

whileがあると、C言語の出力には while ( と出力します。

<condition>は、条件式、例えば a > 1 のような比較式を書く処理です。その条件が成立している間はループします。

do は、C言語の出力では ) で条件式の終わりを出力します。

最後に<statement>で、1行の実行処理ができるのですが、if文の説明でも示したようにbegin, endを使えばここに複数の実行処理も書けます。


PL/0言語ソース
    while a > 1 do
      a := 1

  ↓

C言語出力
    while (a > 1)
      a = 1;


2.4.4.6 print文

次は、計算結果などを表示するためのprint文です。


                |
                  "print"       <emsg "print sentence. ">
                   [
                    (
                      <ident #id>       <print 'printf("%d ", ' #id ');'>
                    | <number #s #n>    <printf 'printf("%s%d ", "' #s'" ,' #n ');'><print>
                    | <strings #str>    <printf 'printf("%s ", "'#str'" );'><print>
                    )
                   { ","
                    (
                      <ident #id2>      <print 'printf("%d ", ' #id2 ');'>
                    | <number #s2 #n2>  <printf 'printf("%s%d ", "' #s2'" ,' #n2 ');'><print>
                    | <strings #str2>   <printf 'printf("%s ", "'#str2'" );'><print>
                    )
                   }
                   ]
                                        <print 'printf("\n");'>



print文では、変数、数値、および文字列を , (カンマ)で区切った構文になります。 文字列は、" または ' で囲まれた文字列の形式です。

PL/0言語は、データ型として文字列を持たないのにも関わらず、文字列がprintできるのは面白いですね。 まあ、そうでないと結果出力が数字だけになってしまい無味乾燥になてしまうので、妥当だと思います。

まず、ソース上では"print"とあってもそのときはコンパイル結果として何も出力しません。

その後、変数名<ident>が来るとそれは変数と解釈され、次のようにprintf文としてコンパイル結果に出力されます。


<print 'printf("%d ", ' 変数名 ');'>

ソース上に数値<number>が来ると次のように出力します。


<printf 'printf("%s%d ", "' #s'" ,' 数値 ');'>

ソース上に文字列<strings>が来ると次のように出力します。


<printf 'printf("%s ", "' 文字列 '" );'>

複数のprint出力を完了した後には、改行を出力します。


<print 'printf("\n");'>

print文の引数がすべて省略され一つも無い場合には、最後の改行出力のみが行われます。 改行だけしたい場合に使います。


PL/0言語ソース
        print "number = ", a;
        print "number = ", 10;
        print

  ↓

C言語出力
    printf ("%s ", "number = ");
    printf ("%d ", a);
    printf ("\n");
    printf ("%s ", "number = ");
    printf ("%s%d ", "", 10);
    printf ("\n");
    printf ("\n");


2.4.5 条件式<condition>と数式<expression>

次は、if文やwhile文で条件の指定に使う条件式と代入文で使う数式の処理について説明します。

PL/0では、条件式や数式の中で使う演算子の次の優先度を持ちます。


優先度高

 = # >= > <= <

 + -

 * /

優先度低

2.4.5.1 条件式

条件式は、数式を不等号で繋いだ形態ですから、まず条件式について説明しましょう。


<condition>     "odd"           <emsg "odd syntax.">
                                <printf "((">
                  <expression>  <printf ") & 1)">
                |
                <expression>
                ("="            <printf " == ">
                |"#"            <printf " != ">
                |"<="           <printf " <= ">
                |"<"            <printf " < ">
                |">="           <printf " >= ">
                |">"            <printf " > ">
                )
                <expression> ;


PL/0では、条件式として、=, #, <=, <, >=, > が不等号として使えます。 =は、等しいことを示し、#は等しくないことを示すのが他のC言語等と異なることに注意してください。

加えて、組み込み関数としてodd(数式)が使えます。 oddは、数式の計算結果が、偶数か奇数か判定する関数で、奇数の場合に条件が真になります。

上の定義で示したように、odd関数は、1とビット論理積& をとるコードを出力することによって実現します。


PL/0言語ソース
        odd(a+10)

  ↓

C言語出力
    ((a+10)&1)

このoddの変換で数式部分が()で囲まれているのが重要です。出力されたC言語上で()がないと正しい判定ができないからです。 上の例では、((a+10)&1) と (a+10&1) では後者では+よりも& のほうがC言語では演算子の優先度が高いので結果が変になりますね。

不等号については、それぞれ対応するC言語の不等号の出力に変換されます。


PL/0         →  C言語 
=            →  == 
#            →  != 
<=           →  <= 
<            →  < 
>=           →  >= 

2.4.5.2 数式

次は数式の処理について説明します。

この数式の処理は手続き型言語などの多くのプログラム言語でも代入文などに使われる中心的な処理です。


<expression>    [ "+"           <printf "+">
                 |"-"           <printf "-">
                ] <term> {
                        ("+"    <printf "+">
                        |"-"    <printf "-">
                        ) <term>};
<term>          <factor> {
                ("*"            <printf "*">
                |"/"            <printf "/">
                ) <factor>};
<factor>        <ident> | <number>
                | "("           <printf "(">
                        <expression>
                  ")"           <printf ")">
                ;


数式の処理は、<expression>, <erm>, および<factor>の3つの要素で構成されています。

<expression>は、加減演算子の+, - を処理します。

<term>は、乗除演算子の*, / を処理します。

<factor>は、変数、数値、または式の中で使われる ( ) を処理します。

<condition>も含めると、

<condition>は不等号演算子(=, #, >=, >, <=, < )を処理して<expression>を呼び出して、

<expression>は加減演算子(+, - )を処理して<term>を呼び出して、

<term>は乗除演算子(*, / )を処理して<factor>を呼び出して、

<factor>は、もっとも基本的な要素である、変数、数値、( ) で囲まれた数式を処理します。

これは、それぞれの述語で処理されている演算子を見てもらうと判ると思いますが、呼び出し関係によって演算子の優先度が決まることに注意してください。 上位の述語から呼ばれた処理のほうが優先度が高くなります。

( ) は最も優先度が高い要素ですが、中の数式<expression>を再帰的に処理することによって、3 * (1 - a)のようなカッコ付きの式を優先して処理することを示します。

ただし、PL/0の加減乗除の演算子やカッコはC言語と同じ記号を使うので、出力にはそのまま演算子、カッコ、変数名や数値を出力するだけです。

<fuctor>の中の<indent>述語は変数名とマッチするとその名前をそのまま出力します。 同様に<number>は数値とマッチするとその数値をそのまま出力します。


PL/0言語ソース
    3 * (1 - a)

  ↓

C言語出力
    3 * (1 - a)

式の出力はPL/0もC言語も同じですが、PL/0コンパイラで処理することによって数式に構文エラーがないことが保証されます。

2.4.6 その他の処理
2.4.6.1 ident(変数名、手続き名)

identは、変数名や手続き名とマッチするか確認します。


<ident #id>     <ID #id>
                <reserved #id>
                ;
<ident>         <ident #id>
                <printf #id>
                ;


引数のあるident述語は、組み込み述語IDを呼び出し、マッチした名前が予約語と一致しないかreserved述語で確認します。 予約後と一致した場合は、処理が失敗し変数や手続き名としては扱われません。

引数のないident述語は、引数ありident述語を呼び出し、その結果の名前をprintfで出力します。

引数ありの述語は、引数をプログラム内で受けて処理するときに使います。

引数なしの述語は、マッチした変数名または手続き名を即座に自動的に出力してしまう場合に使います。

このあたりの使い分けについては、前の項のほうのいろいろな箇所でident述語の使われている処理を再度確認してみてください。

2.4.6.2 number(数値)

numberは、数値とマッチするか確認します。


<number #sign #n>
                ["+"            <is #sign "">
                |
                 "-"            <is #sign "-">
                |
                                <is #sign "">
                ]
                <NUM #n>
                ;
<number>        <number #sign #n>
                <printf #sign #n>
                ;

引数のあるnumber述語は、先頭に単項演算子の + や - が付いていないか確認してから組み込み述語NUMを呼び出し数値とマッチするか調べます。 + または - が点いていた場合は#signに設定し、なければ符号は空文字列""とします。

引数のないnumber述語は、引数ありnumber述語を呼び出し、その結果の数値を符号付きでprintfで出力します。

2.4.6.3 strings(文字列)

stringsは、文字列とマッチするか確認します。


<strings #str>  <STRINGS #str>
                ;
<strings>       <strings #str>
                <printf #str>
                ;

identやnumberと同様にこれも引数ありとなしの形式があります。

引数のあるstrings述語は、組み込み述語であるSTRINGS述語を呼び出してマッチする文字列を引数に設定します。

STRINGS述語にマッチする文字列は、' ' または " " で囲まれた文字列です。

引数のないstrings述語は引数ありのstrings述語を呼び出して、printfで文字列を出力します。

2.4.6.4 reserved(予約語チェック)

reserved述語は、引数#idが、プログラム言語の予約語かどうか判定します。

これは、変数名や手続き名が予約語とマッチしてしまった場合にエラーとする場合に使います。


<reserved #id>
                <not ::sys <member #id
                  (var procedure begin end if then while do call print odd)>>
        ;

組み込み述語であるmember述語は、第1引数が第2引数のlistの中のメンバーかどうか判定します。

そのmember述語をやはり組み込み述語であるnotで囲むことによって意味を逆にして、#idが予約語であった場合にはreserved述語はFailになります。

2.4.6.5 emsg(エラーメッセージ)

emsg述語は、エラー時のメッセージを処理します。


<emsg #x>
                <x <getVar #Line Line>
                   <warn "error : " #Line ":" #x>
                   <exit>>
                ;

emsg述語は、エラー発生時に行番号とエラーメッセージを表示してexit終了します

組み込み述語のx述語を使っていることに着目してください。

x述語は、正常に実行されるときはなにも処理を行わない述語です。

しかし、その後の処理で処理が失敗してバックトラックしたときには引数の処理が実行されるのです。 デカルト言語では、処理が失敗するとバックトラックして前の処理に戻って別の処理を再開しようとします。 このバックトラックが起こったときにx述語を再実行するとx述語の引数の処理が実行される仕掛けになっています。

そのため、<emsg メッセージ>の後の処理で実行が失敗すると引数の処理で実行中の行番号を求め、組み込み述語のwarn述語でメッセージを表示してから、exit述語でエラー終了させることができます。


~

  <emsg メッセージ>

×  emsg述語の後の処理が失敗するとemsg述語まで戻ってエラー処理実行


さあ、これでPL/0言語のソースの説明がすべて完了しました。

次の章からは、PL/0言語を元にしてさまざまな改造を施していきましょう!