BASICとは

初心者向け汎用記号命令コード(Beginner's All-purpose Symbolic Instruction Code)の略とされるプログラミング言語で、FORTRANを極端に簡略化して設計された言語です。

MITS社のアルテア上で動作するBASICインタプリタをビル・ゲイツらが作成し、これがかのマイクロソフトの創業につながったというのは有名です。その後、1970年代後半から1980年代前半にかけて、世界中で発売されたパーソナルコンピュータのほとんどすべてでマイクロソフト社製のBASICインタプリタが動いていたと言っても過言ではないくらい、BASICは普及しました。

日本では1970年代の末に日立のベーシックマスターシリーズ、NECのPC-8000シリーズといったパソコンが発売され、BASIC言語でさまざまなアプリケーションが開発されていました。

その後、BASICは様々な拡張が施され、現在もVisual BASIC .NETなどの処理系が利用されています。

本稿は、Visual BASICなどの邪道はひとまず忘れて、古き良き1980年前半頃に流行していたBASICを題材にします。

私が初めて手に入れたパーソナルコンピュータはNEC製のPC-6001と言う機種で、このマシンはスイッチをONにすると、N60-BASICが起動しました。N60-BASICは、データ型としては単精度実数型と文字列型だけしか持たず、先代のN-BASICをかなり簡略化した仕様の処理系でした。

その後2ヶ月も経たないうちにPC-6001では満足できなくなり、アルバイトをして富士通製のFujitsu MICRO-8 (FM-8) というマシンを手に入れました。このマシンのF-BASICは、当時のフルスペックのBASICで、これから紹介するBASICは、この辺のBASICを念頭に置いています。Visual BASICにあてはまる話もあればあてはまらない話もありますので、もし試すときには注意が必要です。

言語仕様

お約束の「Hello, World」

10 PRINT "Hello, World!"
20 END

BASICのプログラムは行番号と実行文からなる「行」の列として定義します。行番号はすべての行でユニークでなければなりません。処理系によって勝手に昇順に並べられ、通常は一番小さい行番号の行から実行が開始されます。

BASICに対して番号とテキストを入力してENTERキーを押すと、それが行番号と実行文であると解釈され、メモリに蓄積されます。このとき入力された行番号が、まだメモリ上に存在しない行番号であれば行の追加となり、既存の行番号であれば行の置き換えになります。また、既存の行番号だけを入力してENTERを押すと、その行はメモリから削除されます。そういった作業を繰り返して「プログラミング」を行うことになります。

一方、BASICに対して行番号なしでコマンドを入力すると、それはプログラムとして蓄積するのではなく、直接実行せよ、という意味になります。

冒頭の2行からなるプログラムを入力した後、このプログラムを実行するには、例えば「RUN」とだけ入力し、ENTERを押します。

予約語

多くの他の言語と同様に、識別子とは区別される予約語が定義されています。と言いますか、BASICの場合、標準の名前と言う考え方がなく、コマンドの名前や関数名など、予め定義されている名前のありとあらゆるものが予約語となっています。

データ型

整数型、単精度実数型、倍精度実数型、文字列型の4つの型があります。 ユーザ定義型はありません。

また、誤解を恐れずに言うと配列型などという考え方もありません。

変数

変数には単純変数と配列変数があります。配列は何次元でもありです。

単純変数の場合、変数宣言はなく、いきなり初出の変数に代入するというスタイルとなります。代入せずにいきなり参照することも可能で、その場合変数の内容は0、または文字列型の場合は空文字列と言うことになっています。

配列変数は、通常DIM文を使用して次元を定義してから使います。例えば、

10 DIM A(10)
20 FOR K = 0 TO 10
30   A(K) = K
40 NEXT K

とすると、配列変数A(0)A(10)の中身が0~10となります。DIM文で定義するときに指定する数は、配列サイズではなく、添え字の上限となります。そして、上の例のように上限が10のDIM文は省略可能です。つまり、いきなり配列変数を使用すると、その時点で「DIM 変数名(10)」が実行されたかのように動作します。

変数の型指定は、接尾辞によって行えます。初期状態で既定の型は単精度実数型となっており、上のプログラムの配列変数Aや単純変数Kは、どちらも単精度実数型となりますが、変数名の後ろに「%」を付与すると整数型、「#」を付与すると倍精度実数型、「$」を付与すると文字列型の変数となります。単精度実数を明示するには「!」を付与します。

既定で単精度実数型と言う動作は、DEFxxx文で変更可能です。先ほどのプログラムの前に

5 DEFINT A-Z

を追加すると、配列変数Aも単純変数Kも、どちらも整数型となります。DEFINTDEFSNGDEFDBLDEFSTRは、それぞれ既定で整数型、単精度実数型、倍精度実数型、文字列型となる変数の頭文字を定義します。

変数は、同じ名前でも型が違えば別々の変数とみなされます。また単純変数と配列変数も同様です。ですので、

A = 1
A(0) = 2
DEFINT A
A = 3
DEFSNG A
PRINT A

を実行すると「1」が表示されます。

古き良きBASICには局所変数というような概念はありませんので、あらゆる変数は一度作成するとCLEARして全変数を消去するまで、ずっとメモリ上に残ります。

行の構造

前述の通り、「行番号 実行文」というのが基本ですが、マルチステートメントという機能があり、実行文のところには「:」で区切って文を複数書けます。変数の説明のところに出てきた例は、

10 DIM A(10):FOR K = 0 TO 10:A(K) = K:NEXT K

のように一行で書けます。

制御文

  • GOTO文

悪名高きGOTOです。GOTO 100で行番号100にジャンプします。行番号100の行が存在しなければ、「Undefined Line Number」というエラーメッセージが表示されます。

  • GOSUB文

サブルーチンを実現するための命令で、GOSUB 100で行番号100にジャンプするのはGOTOと同様ですが、その後RETURNが実行されると、GOSUB命令の次の命令に戻ってきます。

  • IF文

「IF 条件 THEN 真のとき ELSE 偽のとき」という文で分岐が可能です。「真のとき」は実行文かまたは行番号を書きます。行番号を書くとそれは「GOTO 行番号」と解釈されます。またもう一つバリエーションがあって、「IF 条件 GOTO 行番号」というのも可能です。

BASICのIF文はGOTO文といい勝負ができるくらい悪名が高く、何が何でも一行に書かないといけないという制約があります。複数行に渡って処理を分岐したいときには、結局GOTOする羽目になります。

  • FOR文

繰り返しの制御文です。「FOR K = 9 TO 17」とすると「NEXT K」を実行するまでの間のプログラムを、変数Kを9から17まで1ずつ増やしながら実行します。構造化プログラミング言語にありがちなFOR文と類似の制御構造ですが、正統派BASICはあくまで非構造化言語ですから、実はALGOL系のFOR文とは、かなり趣が異なります。

例えば1から10の数について、偶数なら「EVEN」、奇数なら「ODD」と表示するには、

10 FOR K = 1 TO 10
20   IF K MOD 2 = 0 THEN PRINT "EVEN" ELSE PRINT "ODD"
30 NEXT K

などとなりますが、これを無理やり一行で書くとこうなります。

10 FOR K = 1 TO 10:IF K MOD 2 = 0 THEN PRINT "EVEN":NEXT K ELSE PRINT "ODD":NEXT K

驚くべきことにFOR文の終わりを示すはずのNEXTが2ヶ所に分散しています。「FOR」~「NEXT」は、構造化言語のように枠構造を作る構文ではなく、「FOR」を実行した後に「NEXT」に出会うと、最後に実行して終了していない「FOR」に戻るという動作を行うだけの独立した「命令」に過ぎません。

※BASICにもいろいろな処理系があり、純粋なインタプリタではなく、ある程度コンパイラ的な動作を行う処理系も存在します。というより、今となってはむしろそちら方が一般的です。そのような処理系の場合、上記のようにFORとNEXTが1対1に対応していない構造は許さないでしょう。

エラー処理

今時の言語風に言えば例外処理ということになりますが、BASICでもエラー処理が可能です。

ON ERROR GOTO 9000

を実行すると、以降の処理で何らかのエラーが発生すると番号9000の行に制御が移ることになります。そして、その中でERL、ERRという変数(とはちょっと違うが)を参照すると、それぞれエラーが発生した行番号、エラーコードが読めますので、適切な回復処置を行うという段取りです。

エラー処理が終わってRESUME命令を実行すると、元の場所から処理が再開します。

ON ERROR GOTO 0

を実行すると、エラー処理をキャンセルします。このとき、エラーが発生していてRESUMEする前であれば、ただちにエラーメッセージが表示されてプログラムがストップします。ちなみに、実際の行番号に0は使えません。

C/C++言語などに負けず劣らず、BASICの式はかなり複雑です。 演算子の優先順位で言うと、高い方から順に以下のようになります。

  1. べき乗を表す「^」
  2. 符号を表す「+」、「-」
  3. 乗除算を表す「*」、「/」
  4. 整数除算を表す「\」
  5. 剰余を表す「MOD」
  6. 加減算を表す「+」、「-」
  7. 関係演算を表す「=」、「<>」、「<」、「<=」、「>」、「>=」
  8. 論理否定を表す「NOT」
  9. 論理積を表す「AND」
  10. 論理和を表す「OR」
  11. 排他的論理和を表す「XOR」
  12. 含意を表す「IMP」
  13. 同値を表す「EQV」

この優先順位列に、「(」...「)」や「SIN」などの関数を入れたがる人が結構いますが、それは演算子ではないのでいい加減に止めてもらいたいです。

※1 SHARPのプログラム電卓にSIN 0.2のような書き方を許すBASICがありましたが、これだと単項前置演算子と見なせます。

※2 C/C++のグルーピングの「(」...「)」ではなく関数呼び出しを意味する「(」...「)」であれば、これは立派な単項後置演算子と見なせます。

脱線しました。

BASICの演算子の優先順位の話に戻りますが、この順位の中で単項演算子のNOT + - は単純に優先順位で片付けられない文法となっています。この3つを除く残りすべての二項演算子は左結合となっていて、

2^3^2 ... (2^3)^2 ... 64

と計算されますが, 2^-1^22^(-1)^2 ではなく,

2^-1^2 ... 2^(-(1^2)) ... 0.5

となります。おそらくビル・ゲイツは演算子の優先順位テーブルで処理したのだろうと想像しますが、再帰下降型パーサでこの仕様は若干辛いものがあります。

世の中はC言語の流行以来、すっかりC言語流の仕様がのさばっていますが、BASICの「3 / 2」は「1」ではなく、正しく「1.5」となります。

低レベル機能

機械語を実行したり、番地指定でメモリを読み書きしたりする機能があります。メモリを読みだすにはPEEK関数、書き込むにはPOKE命令、機械語を実行するにはEXEC命令やUSR関数が用意されています。

100 CLEAR , &H5000
120 POKE &H5000, &H86
130 POKE &H5001, &H03
140 POKE &H5002, &H8B
150 POKE &H5003, &H04
160 POKE &H5004, &HB7
170 POKE &H5005, &H51
180 POKE &H5006, &H00
190 POKE &H5007, &H39
210 EXEC &H5000
220 PRINT PEEK(&H5100)
230 END

のようなプログラムで、気軽に機械語の実験ができて重宝します。ちなみにこのプログラムは、MC6809マシン用で、3+4を計算し、結果を表示します。

BASICの変数の中身を直接操作するための機能もあります。VARPTR(A)は、変数Aの値が格納されているアドレスを返す関数です。

POKE VARPTR(A%), 1:POKE VARPTR(A%)+1, 2

を実行すると、変数A%&H0102という値が入ることになります。もちろんこれだけなら

A% = &H0102

と書く方が手っ取り早いですが、例えば配列の格納アドレスを機械語に渡して、直接書き換えるなどと言うことができるようになっているわけです。

メモリマップ

8ビットマシン全盛期に一世を風靡したBASICは、とにかく実装もランタイムデータもコンパクトになるように工夫されています。ヒープ上にばんばん変数領域を作って、ときどきガベージコレクション、などと言うズボラは絶対に許されなかった時代ですので。

以下のようなメモリマップが一般的です。

インタプリタ・ワーク
プログラム・エリア
単純変数エリア
配列変数エリア
空き(1)
スタック・エリア
文字列エリア
空き(2)

メモリの低位










BASICが管理するメモリの上限
メモリの上位

変数エリアは、変数名と型と値、配列の場合はそれに加えて次元数と各次元の上限を隙間なく並べます. インタプリタは、変数名と型と配列か否かでこの領域のどこに変数があるかをリニアサーチします。ハッシュ表などの出る幕はありません!

文字列型は特別で、変数エリアには文字列の内容を含まず、長さと文字列の本体を格納した文字列エリア内の位置を持っています。本体を切り離さないと、サイズ違いの文字列を代入するたびに変数エリアを再配置する羽目になるので妥当なところでしょう。

文字列エリアはガベージコレクション前提で隙間を気にせずに使って行き、空きがなくなるといわゆるコンパクションを実行します。正統派BASICではこれはとてもコストの高い処理で、大雑把に言うと、変数エリアに散在する文字列型変数をスキャンして文字列エリアの穴を見つけて移動して詰めるということを繰り返します。

※厳密にはコンパクションはガベージコレクションと区別されるようです。

中間言語コンパイル方式

当時そう宣伝されてはいましたが、まったくそういう機能はありませんでした!予約語や記号などが短いバイナリコードにエンコードされて格納されることを指してそう呼ばれていたのですが、今同じことをこう表現すると「詐欺」だと言われても仕方がありません。

ただ、例えば「GOTO 100」の8文字が「03 64 00」のような3バイトに変換されることでメモリ効率と実行効率の両面でかなり有利だったのは事実です。

今回実装した処理系について

ふと前述の文字列エリアのコンパクションのアルゴリズムを書き留めておこうと思い立ち、ついでにインタプリタごと作ってみようと思ったのがきっかけで、まずはJavaでプロトタイプを書き始めました。

まだまだ未実装機能だらけで、まともには使えませんが、何となく雰囲気は味わえるかも知れません(?)

最近、パーサを手書きするとき、トークンコードをenumで実装するのがマイブームとなっていますが、こうするとトークン毎の処理をenumの中に実装したくなってしまい、気が付くとenumが巨大なソースに膨れ上がってしまっています。enumだと、キーワードをアドイン的に追加したりといった機能が実装し辛いし、ほとんどいいことがありません。次は絶対にやめようと思います。

実装した機能

  • 中間言語コンパイル方式のようなもの
  • データ型の骨組み
  • 単純変数と配列変数(1次元のみ)
  • 式…ほぼフルスペックですが、例えば「32767+32767」が「65534」ではなく「-2」になってしまうことに実装してから気付きました!バグです。

未実装機能(の内、実装したい機能)

  1. IF、FOR、GOSUBなどの制御機能

IFの方式を検討しただけで、実装はまだです。

IF 式 THEN 命令1:命令2 ELSE 命令3:命令4

の実行は、まず式を評価し、真ならTHENを読み飛ばします。IFの実行はそこまでで、あとは通常通り命令を処理していきます。非構造化言語であるBASICでは、IF文の中身の実行に再帰などの出る幕はありません。

命令列を実行していくと、そのうちELSEに出会うかも知れませんが、その場合行末まで読み飛ばして次の行に進みます。

問題は式を評価した結果が偽であった場合です。このときELSEに出会うまでは命令を読み飛ばしていかなければなりませんが、読み飛ばすためには各命令の構造が分かっていないといけません。読み飛ばす専用処理など、いちいち書いていられませんので、ここは「スキップ中フラグ」で何とかならないか検討しました。つまり、式の評価結果が偽なら、スキップ中フラグを立てて終わり、その後命令1、命令2を実行していきますが、各命令がオペランドの解釈などだけを実行するわけです。そしてELSEと出会うとスキップ中フラグをリセットし、命令3以降は通常の実行に戻ります。

これだけではまだ不十分で、今度は

IF 式1 THEN 命令1:IF 式2 THEN 命令2 ELSE 命令3 ELSE 命令4

を考えてみます。式1が真だったときは上の方式だけで大丈夫ですが、偽だったときは1つ目のELSEでスキップをやめてはだめで、2個目のELSEで初めてリセットとなるようにしないといけません。つまり単なるフラグではなく、カウンタが必要です。IFの条件が偽だったときに「1」にして、カウンタが非0のときにIF文に出会うとカウンタを+1、ELSEに出会うと-1します。そして非0の間は各命令や式の評価を全部スキップするというような制御をやればよさそうです。こういう考え方はコンパイラ言語ではまったく出てこない考え方です。勉強になりました。(と言いつつ、まだ実装していないという…)

FORとGOSUBの実装にはスタックが必要で、両者共通のスタックを使います。例えばFORの中でGOSUBを実行し、RETURNする前にNEXTを実行するとエラーとします。

1. グラフィックス機能

これがないと寂しいです。

2. エラー処理機能

今回、全然間に合いませんでした。    スクリプト言語的な用途も考慮して、インタラクティブモードとバッチモードを設け、エラー処理なしでエラーが発生した場合、インタラクティブモード時はプロンプトに戻り、バッチモード時はプロセス自体を終了することにします。この考え方は、以前作成したMATLAB互換処理系でも、IDL互換処理系でも採用していました。ちなみにMATLAB互換処理系では、プログラム中にあたかも手続きを実行するかのようにインタラクティブなプロンプトを出す命令を実装していましたが、この処理系のこのモード管理を考えているときは混乱しまくりました。

3. 多次元配列

あと1日あれば、というところ。難しくはないです。

4. 割込み機能

ON INTERVAL 1 GOSUB xx

で、1秒ごとにxxを呼び出す機能など。

5. その他、諸々

肝心のコンパクションですが、Javaでは書けないので、そのうちARDUINOか何かで動かすときに実験してみようか、などと考え中です。いや、Javaでも巨大なbyte[]を無理やりメモリ領域と見立てて…