* 川合のプログラミング言語自作のためのテキスト第三版#1 -(by [[K]], 2021.01.19) ** (1) はじめに -「プログラミング言語を作ってみたい」と思ったことがある人は、プログラマの中にはけっこうたくさんいるのではないかと思います。・・・でも、実際に作ってみた人はそれほどには多くないかもしれません。それはなぜでしょうか。 -おそらくは、多く人にとってプログラミング言語を作るのは難しいということになっているからだと思います。そう思うからこそ、チャレンジするまでもなくあきらめてしまうわけです。とてももったいないです。 -私はプログラミング言語は(世間の人が思っているよりは)ずっと簡単に作れるものだと思っています。・・・今からここにそのためのテキストを置きます。これを読んで是非作れるようになってください。そうすれば「作りたいけど、作り方が分からない」を卒業することができます。そして今後は「じゃあどんな言語を作ろうか」をメインで考えられるようになるのです。それはとても素敵なことだと思います。 -この時点では「本当にプログラミング言語を作るのは簡単なのか?」という疑問を持つ人は少なくないと思いますが、かつてはタイニーベーシックのような言語があり、それはたった数キロバイトの機械語でした。それらの言語はもちろん言語を作りやすくするために文法規約にいろいろと制限がありましたが、つまり逆に言えば、適切な制限を導入すれば言語は簡単に作れるということなのです。 ---- -このシリーズで説明したいこと: --''[1] プログラミング言語を作るのは難しくない!'' --''[2] JITコンパイラはとても高速。しかも(世間で思われているほどには)難しくない!'' ---- -もくじ(TL-1から順番に読んでいくとわかるように書いています) |ページ名|名前|行数|.exeの大きさ|説明|速度のめやす| |[[a21_txt01]]|TL-1|RIGHT:52行|RIGHT:6.00KB|初めの一歩、たった52行のシンプルすぎる言語処理系|RIGHT:計測不能| |[[a21_txt01_2]]|TL-2|125行|RIGHT:6.50KB|変数名は1文字じゃなくてもOK、見やすいスペースやインデントもOKに|RIGHT:計測不能| |[[a21_txt01_3]]|TL-3|143行|RIGHT:7.00KB|条件分岐などをサポートしてループ処理が可能に|RIGHT:1150倍| |[[a21_txt01_4]]|TL-4|180行|RIGHT:7.50KB|REPLの導入(これは楽しい!)|RIGHT:320倍| |[[a21_txt01_5]]|TL-5|208行|RIGHT:8.00KB|少し高速化|RIGHT:203倍| |[[a21_txt01_6]]|TL-6|305行|RIGHT:8.00KB|もっと高速化(がんばった!)|RIGHT:11.3倍| |[[a21_txt01_6a]]|TL-6a|334行|RIGHT:8.50KB|さらに高速化!(これをやるかどうかは好みが分かれるかも)|RIGHT:6.7倍| |a21_txt01_7|TL-7|||式の導入|| |a21_txt01_8|TL-8|||forやwhileの追加|| |a21_txt01_9|TL-9|||グラフィック命令の追加|| |a21_txt01_10||||ここまでのまとめ・今後の課題|| |a21_txt01_11|TJ-1|||最初のJITコンパイラ|| --(註)「.exeの大きさ」はWindows向けにgcc(MinGW)でコンパイルした時の大きさです。 ---しかしここで扱う言語処理系は、Windows専用というわけではなく、他のOSでも問題なく動作するようになっています。 ---純粋に言語処理系の規模の目安を示すために書いてあります。 --「速度のめやす」は、C言語で10億回ループさせた場合と、この言語で10億回ループさせた場合の処理時間の比を書いています。数が大きいほうが遅いです。 ---なお、gccに対しては「-O3」という一番強い最適化を指示しています。 ---遅すぎて10億回もまわせないときは、1億回とかにへらして、その結果を10倍して記入しています。 ---おおざっぱな目安です。 ** (2) インタプリタかコンパイラか -プログラミング言語は、大きく分けるとインタプリタ型とコンパイラ型に二分できると思います。コンパイラ型は、ソースコードをコンパイラにかけると実行ファイルが出力されてきます(アセンブラソースが出力されるタイプもあります)。実行に際しては、この実行ファイルだけがあればよく、ソースコードもコンパイラ本体も不要です。 -一方でインタプリタ型は、ソースコードを実行前にコンパイルしておく必要はなく、そのまますぐに実行できます。ただ実行に際しては、ソースコードと言語処理系本体の両方が必要です。また実行速度はコンパイラには劣るというのが普通です。コンパイルを必要としないので、気軽に実行することができます。 -今では普通のコンパイラについてはよいテキストが十分にあると思うので、ここではインタプリタをメインに扱おうと思います。 ** (3) TL-1 -うっとうしい説明はこれくらいにして、とにかく言語を作ってみようじゃないですか。これは「TL-1」です。C言語で書いています。 #include <stdio.h> #include <stdlib.h> void loadText(int argc, const char **argv, unsigned char *t, int siz) { FILE *fp; int i; if (argc < 2) { // 引数が少ないのでエラー表示して終了. printf("usage>%s program-file\n", argv[0]); exit(1); } fp = fopen(argv[1], "rt"); // テキストモードでファイルを開く. if (fp == 0) { // ファイルを開けなかった. printf("fopen error : %s\n", argv[1]); exit(1); } i = fread(t, 1, siz - 1, fp); fclose(fp); t[i] = 0; // 終端マークを書いておく. } int main(int argc, const char **argv) { unsigned char txt[10000]; // ソースコード. int i, pc, var[256]; // varは変数. loadText(argc, argv, txt, 10000); for (i = 0; i < 10; i++) var['0' + i] = i; // テクニック#1. for (pc = 0; txt[pc] != 0; pc++) { if (txt[pc] == '\n' || txt[pc] == ' ' || txt[pc] == '\t' || txt[pc] == ';') // 空行など. continue; if (txt[pc + 1] == '=') { // 2文字目が"=". if (txt[pc + 3] == ';') { // 単純代入. var[txt[pc]] = var[txt[pc + 2]]; } else if (txt[pc + 3] == '+' && txt[pc + 5] == ';') { // 加算. var[txt[pc]] = var[txt[pc + 2]] + var[txt[pc + 4]]; } else if (txt[pc + 3] == '-' && txt[pc + 5] == ';') { // 減算. var[txt[pc]] = var[txt[pc + 2]] - var[txt[pc + 4]]; } else goto err; } else if (txt[pc] == 'p' && txt[pc + 1] == 'r' && txt[pc + 5] == ' ' && txt[pc + 7] == ';') { // 最初の2文字しか調べてない(手抜き). printf("%d\n", var[txt[pc + 6]]); } else goto err; while (txt[pc] != ';') pc++; } exit(0); err: printf("syntax error : %.10s\n", &txt[pc]); exit(1); } -はいこれだけです。C言語でたったの''52行''です。もちろん機能は少ないですし、いくつか制限もあります。 --変数名は半角一文字のみ。 --数値定数は整数で一桁のみ。 --しかも「a=3;」みたいにスペースを入れずに書かなければいけない。 --ひとまず足し算と引き算とprintができればいい。 --printは「print 変数名;」の形式のみ解釈できる。このときのスペースは必ず1文字。 -ええ本当にひどい仕様だと思います。・・・かつてのタイニーベーシックよりもさらに低機能です。でもとにかく出発点としては悪くないと思うんです。だってたったの52行ですよ? -しかも<stdio.h>と<stdlib.h>しか使っていません。それでも52行でここまでできるんです。 --<stdio.h>はprintf()とファイル入出力のために使っています。 --<stdlib.h>はexit()のために使っています。 -このTL-1は以下のプログラムを問題なく実行する能力があります。以下の内容のテキストファイルを作って保存して(たとえば prog1.txt )、そのファイル名をTL-1のコマンドライン引数に渡せば実行できます。 a=1; b=2; c=a+b; print c; -今は足し算と引き算しかないですが、掛け算や割り算を追加するとしたらどうしたらいいかわかりますか?分かりますよね。 -この先で、最初に課した制約を少しずつ減らしていきます。するとだんだんマシな言語になります。差分を追いかければ、どの制約をなくすことが言語を複雑にしているのか、理解できるようになるでしょう。 ** (4) TL-1の簡単な説明 -関数: --loadText() : コマンドライン引数で指定されたソースファイルを配列変数に読み込む。 --main() : 言語処理の本体。 -変数: --var[] : 変数の値を記憶しておくための変数。 --txt[] : ソースコードを記憶しておくための変数。 --pc : 現在プログラムのどこを実行しているのかを記憶するための変数。 -プログラム中で「テクニック#1」と書かれた部分があります。ここがやっていることは、「3」という定数さえも「3」という名前の変数であると解釈することにして、その代わり変数「3」には初期値として3を代入しておくのです(そうでないと変数「3」を参照したときに3にならなくなってしまうため)。 --こうすることで、プログラムでは以降変数と定数を区別する処理が不要になって、TL-1は短く書けるようになります。 -''C言語での文字列の扱いに不慣れな人のために、簡単な説明ページを作りました。必要な人はご利用ください。→[[a21_txt01_1a]]'' -[隠れたこだわり] --このTL-1やそれ以降のプログラムは、C言語が得意じゃない人でも分かりやすくなるように一定の配慮をしています。 --ポインタはやむをえないときは使ってもいいが、ポインタに対する加算や減算はしない(ポインタは配列みたいなものだと理解している場合、ポインタに対する演算があるとそこで難しく感じてしまうので)。 --文字列を扱うときはなるべく標準関数で扱える形式にする。 ** 次回に続く -次回: [[a21_txt01_2]] *こめんと欄 #comment