2023年5月20日土曜日

コンパイラ作成の試み その1「ハイクラスC言語」のリストを入力



 コンパイラを作ってみたくて「ハイクラスC言語」のリストを入力してみました

昔からコンパイラの自作に憧れていまして、入手した書籍は十数冊になると思いますが、本をいくら読んでも(もちろん、完全に理解できるまで読み込めていないのですが)、コンパイラ(あるいはインタプリタ)の構造についてはおぼろげには理解できたような気がしても、実際のプログラムをどのように構成すればよいのかが良く分かりませんでした。

目標は6809用の整数型のコンパイラの自作なので、特に具体的なコード生成手法を知りたいのですが、何しろコンパイラの本というのは理論的な本が多くて、、、


今までに読んだ本で印象に残っている本を挙げると、


・明解入門 コンパイラ・インタプリタ開発 (林晴比古 SoftBank社)
 分かりやすかったのですが、変数の使える電卓プログラムまででめげました。

・Go言語でつくるインタプリタ (Thorsten Ball著、設楽洋爾訳 技術評論社)
 段階ごとにテストで確認しながら進む手法に感心しましたが、使ったことがないGo言語に戸惑いながらでしたので、途中で力尽きました。

・コンパイラ 作りながら学ぶ (中田育男 オーム社)
 言わずと知れた中田先生の本です。この本で扱われているPL/0をFLEX9上のMicroCコンパイラを使用して6809に移植しようと試みたのですが、私には無理でした。

・Z80CPU対応 新言語作成の技法 (大貫広幸著 MIA社)
 ほとんど使用経験のないZ80の本ですが、この本で初めてコード生成の具体的手法を知りました。後半はPC-8801とCP/M-80で動作するコンパイラ言語Stellarのアセンブルリストが掲載されています。(いつかCP/M-80版を入力してみたいです。120ページ以上ありますが

・ハイクラスC言語 (末石吾朗、小林優 技術評論社)
 具体的なコード生成に重点を置いているので読みやすいです。仮想マシンの命令コードを設定して、それに対応するコンパイラと、生成された仮想コードを解釈・実行するインタプリタのソースリストが掲載されています。

何冊読んでもなかなか進めないので、思い切って作り始めようと考えたのですが、実は今までにも始めながら途中で投げ出したことが数回ありますので、ゼロから作り始めるのは無理だと考えて、大昔に購入して本棚に入れたままになっていた「ハイクラスC言語」中のリストをまず入力して、その仮想マシンの部分を6809に置き換えるという方針で始めることにしました。




何とプログラムリストの一部が欠落していた


コンパイラプログラムmccとインタプリタプログラムmaiを入力してみました。
入力してみて初めて分かったのですが、何とmccの8個のファイルのうちの c_dec.c のリストが抜け落ちているのです。。。(誤植にしてはヒドイ...)
ただ、その中に3つの関数があることは分かっていて、それらの処理内容も本文中に書かれているので、既に他のファイルは入力してしまったこともあり、何とか自分で記述するしかないと覚悟をしました。
(ちなみに、所有しているのは平成5年の初版で誤植が非常に多いのですが、30年前のものなので、出版社のサイトを見ても関連する資料が見つかりません...)

作成しなければならない関数は、変数宣言を処理する vardec()、関数を処理する function()、文をコンパイルする statement()の3つです。
その仕様についての解説です。

vardec関数の仕様

function関数の仕様

statement関数の仕様


四苦八苦しながら2週間近くかかって何とかエラーが出ない状態にすることができましたが、その過程でプログラムの構造について結構理解が進んだような気がしますので、結果的には良かったと考えることにします。


30年も前の書籍ですし、今更リストを入力しようとする方もおられないとは思いますが、私が書いた c_dec.c を下に示しておきます。(書籍中のリストにこれを追加することで、一応、正常に動作すると思います。)

/***********************************************************************
 ****																****
 ****	c_dec.c	Micro C Compiler declaration procedure file	    ****
 ****																****
 ***********************************************************************/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include "c_mcc.h"

/***************************************************************
 *		vardec()		変数宣言の処理
 *			戻り値:	なし
 *			引数:		なし
 ***************************************************************/
void vardec(void)
{
	do {
		getsym();
		if (sym != IDENT)
			puterr("Variable ID expected.");
		getsym();
		if (sym != LBRCKT) {	/* 単純変数 */
			enter(VAR_SMPL);
			gen(VAR_SMPL, lblcnt++);
		}
		else {					/* 配列変数 */
			getsym();
			if (isdigit(sym) != 0)
				puterr("Number expected.");
			enter(VAR_ARY);
			gen(VAR_ARY, lblcnt++);
			
			getsym();
			if (sym != RBRCKT)
				puterr("']' expected.");
			getsym();
		}
	} while(sym == COMMA);
	
	if (sym != SEMICOL)
		puterr("';' expected.");
	getsym();
}

/***************************************************************
 *		function()		関数処理
 *			戻り値:	なし
 *			引数:		なし
 ***************************************************************/
void function(void)
{
	int i, mflag;
	int j;
	
	source_f();
	if ((i = position()) < 0) {
		label(lblcnt++, fp);
//		enter(FUNCID);
		enter(UNDEFID);
	}
	else {
		if (strcmp(id, "main") == 0) {		/* if "main" */
			j = 0;
			while (strcmp(idtbl[j].name, id) != 0 && j <= idcnt) {
				j++;
			}
			if (j < idcnt) {				/* already exist */
				if (idtbl[j].kind != UNDEFID) {
					puterr("Symbol redeclarated.");
				}
				else {
					mflag = ON;
					label(0, fp);
//					enter(BLTFUNC);
					idtbl[j].kind = BLTFUNC;
					idtbl[j].addr = j;
				}
			}
		}
		else if (idtbl[i].kind == UNDEFID) {
			label(idtbl[i].addr, fp);
//			enter(FUNCID);
			idtbl[i].kind = FUNCID;
//			idtbl[i].addr = lblcnt;
		}
		else
			puterr("Symbol redeclarated.");
	}
	
	getsym();
	if (sym == LBRACE) {
//		i = 0;
//		while (strcmp(id, idtbl[i].name) != 0 && i < idcnt)
//			i++;
//		label(idtbl[i].addr, fp);
		getsym();
		while (sym != RBRACE) {
			statement();
			if (sym == EOFSYM)
				puterr("'}' expected.");
		}
//			if (strcmp(id, "main") == 0 && mflag == ON) {	/* if "main" */
			if (mflag == ON) {	/* if "main" */
				gen(JUMP, 1);
				mflag = OFF;
			} else
				gen(RET, DUMY);
//		}
	}
	else
		puterr("'{' expected.");
	
	getsym();
}

/***************************************************************
 *		statement()		文処理
 *			戻り値:	なし
 *			引数:		なし
 ***************************************************************/
void statement(void)
{
	switch (sym) {
		case IDENT:
					assign();
					break;
		case IFSYM:
					if_proc();
					break;
		case WHILESYM:
					while_proc();
					break;
		case DOSYM:
					do_while();
					break;
		case CALLSYM:
					call_proc();
					break;
		case INPUTSYM:
					input_proc();
					break;
		case PRINTSYM:
					print_proc();
					break;
		case PRTLNSYM:
					print_proc();
					gen(NEW_LINE, 0);
					break;
		case LBRACE:
					lbrace();
					break;
		case SEMICOL:
					getsym();
					break;
		default:
			puterr("statement expected.");
	}
}



わずか160行ほどのプログラムの作成に2週間もかかってしまって情けないですが、他の関数の動作を推測・確認しながらでしたので、こんなものかなと、、、

入力して出来上がった言語mcコンパイラ・アセンブラの仕様ですが、2バイトの整数型のもので、次に示したhit & blowの例で大体の雰囲気が分かってもらえるかと思います。

var i, j, inp, cnt;
var ans[4], in[4];
var hit, blow;
var flag[10], endflag, once_more;

main
{
println "*** Hit & Blow ***";
println "Guess 4 digit number (Miunus to abort)";
do {
call game;
print "Once more ( YES: 1, NO: other ) ? ";
input once_more;
} while (once_more == 1);
println "See you again !";
}

game
{
call init;
cnt = 1;
endflag = 0;
do {
call inpnum;
if (inp < 0)
endflag = 1;
else
call compare;
cnt = cnt + 1;
} while (endflag == 0);
}

init
{
i = 0;
while (i < 10) {
flag[i] = 0;
i = i + 1;
}
i = 0;
while (i < 4) {
ans[i] = rnd % 10;
while (flag[ans[i]] != 0)
ans[i] = rnd % 10;
flag[ans[i]] = 1;
i = i + 1;
}
}

inpnum
{
do {
print "Guess ? ";
input inp;
if (inp >= 10000)
println "Too Big. Re-enter!";
} while (inp >= 10000);
in[0] = inp / 1000;
in[1] = (inp % 1000) / 100;
in[2] = (inp % 100) / 10;
in[3] = inp % 10;
}

compare
{
hit = 0;
blow = 0;
i = 0;
while (i < 4) {
j = 0;
while (j < 4) {
if (ans[j] == in[i]) blow = blow + 1;
if (j == 1) if (ans[j] == in[i]) hit = hit + 1;
j = j + 1;
}
i = i + 1;
}
blow = blow - hit;
print cnt, " times.  ";
if (hit == 4) {
println "Match !!";
endflag = 1;
}
else println "Hit=", hit, " Blow=", blow;
}

このコンパイラの拡張例として、言語仕様の拡張(関数の引数、戻り値、ローカル変数の追加)や、特定CPUのコードを生成するための手順が解説されていますので、これをベースにして、仮想マシンのコードを生成している部分を6809の命令に置き換えることができれば6809のコンパイラができるはずですので、ぜひ実現させたいと思っています。

以上、書籍「ハイクラスC言語」中のコンパイラ・インタプリタのリストを入力してみたという紹介でした。




2 件のコメント:

  1. I've always wanted to create a 6809 compiler for my TRS-80 CoCo. Thanks for writing this blog. I'm using Google translate to read it. Ganbatte kudasai!

    返信削除
    返信
    1. I'm so happy that there are people who have the same aspirations.
      I would like to create a 6809 compiler somehow.
      Also, I'm interested in nitros9, which works with CoCo.

      削除