Skip to content

[投稿]JavaScriptでBASICっぽい言語をつくる

はじめまして。yayugu(http://twitter.com/yayugu)と申します。wise9に送る良いネタが思いついたので送ります。

JavaScriptでBASICっぽい言語をつくる

きっかけ

>日経ソフトウェアあたりが唯一残った投稿雑誌っぽいけど、ちょっと内容が実用向きすぎる。
>もっとホビーっぽいものが欲しい。という僕の一方的な思い込みから来ている。

これを読んでベーシックマガジンが思い浮かびました。直接は知らないのですが、上の世代の方がプログラミングを始めたきっかけとしてN88-BASICやベーマガはよく挙げられますね。
面白いゲームとソースコードが両方手に入るなんてなんて素晴らしい!うらやましいので自分でBASICをつくってみることにしました。

ぼくのかんがえたBASIC

とはいえ、そもそもBASICがどんな言語なのかよく知りません。折角なのであっちこっちで見たコード片のぼんやりした記憶を頼りにつくってみます。

ぜんぜんBASICにならなかったらすみません。

続きは以下のリンクから

BASICってこんなイメージ


A=1
B=2
C=A+B
PRINT C

'くりかえし
*LOOPC:
C=C+1
PRINT C
IF C<100 THEN GOTO LOOPC

'
サブルーチン
GOSUB SUBROUTINE
*SUBROUTINE:
'いろいろ処理
RETURN

こんなかんじだった気がする。

環境

MacのFirefox+Firebugで開発しています。Chromeでも動作するのを確認しました。どちらでもコンソールを開いておいてください。

てはじめ

まずbasic.htmlとbasic.jsを用意します。
basic.html:


<html>
  <head>
    <script type="text/javascript" src="http://code.jquery.com/jquery-1.5.min.js"></script>
    <script type="text/javascript" src="./basic.js"></script>
  </head>
  <script type="text/basic" id='code'>
PRINT HELLO,WORLD!
A = 1
B = 2
  </script>
  <body>
    <canvas id="display" width="240" height="320" style="border-width:1px; border-color: #AAAAAA; border-style: solid;"></canvas>
  </body>
</html>

内に直接BASICコードを書いちゃってます。scriptタグを使うことで<とか>記号をエスケープせず使えたりと良いことがあります。
canvasはあとで使います。

basic.js:


$(document).ready(function(){
  'use strict';

  var code = $('#code')[0].innerHTML;
  console.log(code);

});

$(‘#code’)[0].innerHTMLでHTMLからBASICのコードが抜き出せます。

ここまでできたらこのファイルにアクセスしてみましょう。
explorerとかfinderでダブルクリックかfile:///フォルダ/basic.htmlとURLを打ち込むと開けます。

コンソールに

PRINT HELLO,WORLD!
A = 1
B = 2

と出力されたら大成功!
コンソールはこの画像の左下のやつです。ちょっと分かりづらいので注意。

インタプリタの開発

文法

BASICのコードを実行するインタプリタをつくっていきます。まずはBASICのコードの文字列を適切に区切って、解釈する「パーサ」を書きます。

パーサを簡単にするためにBASICの文法を次のように決めます。

・1行に1命令かく
・命令のキーワード・パラメータはすべてスペース’ ‘で区切る
・うまく解釈できない行は無視する
・(とりあえず)行の先頭にインデントなどで空白は入れない

区切る

まずは文字列を区切ります。改行で区切って、空白で区切るだけ。らくちん


$(document).ready(function(){
  'use strict';

  var evalBasic = function(code){
    var parse = function(str){
      var lines = str.split("\n"); // 改行で区切る
      var ops = [];
      var i;
      for(i = 0; i < lines.length; i++){
        ops.push(lines[i].split(" ")); // スペースで区切る
      }
      return ops;
    };

    var ops = parse(code);
    console.log(ops); // 結果を出力してみる。
  };

  var code = $('#code')[0].innerHTML;
  evalBasic(code);

});

それっぽくなってますね。先頭と最後にゴミが入ってますが無視するので「・うまく解釈できない行は無視する」というルールを決めたので無視できます。

命令を解釈して実行

配列にまとまった命令を読んでいきましょう。
まず1行=1命令をよんで実行する evalOp() を定義します。


var evalOp = function(op){
  switch(op[0]){
    case 'PRINT':
      console.log(op[1]);
      break;
  }
};

まずはPRINT文だけ。こんな風に呼び出します。

evalOp(['PRINT', 'Hello,World!']);

Hello, World! 完成

で、これをopsをfor文で回して各命令に順番にこれを呼べぶ run() を定義すれば、ようやく! やっと!BASICのコードが実行できるようになりました。


$(document).ready(function(){
  'use strict';

  var evalBasic = function(code){
    var pc = 0; // run()の中で使うpcがなんで外側にあるかは後述

    var parse = function(str){
      var lines = str.split("\n"); // 改行で区切る
      var ops = [];
      var i;
      for(i = 0; i < lines.length; i++){
        ops.push(lines[i].split(" ")); // スペースで区切る
      }
      return ops;
    };

    var evalOp = function(op){
      switch(op[0]){
        case 'PRINT':
          console.log(op[1]);
          break;
      }
    };

    var run = function(ops){
      for(; pc < ops.length; pc++){
        evalOp(ops[pc]);
      }
      console.log("END");
    };

    var ops = parse(code);
    run(ops);
  };

  var code = $('#code')[0].innerHTML;
  evalBasic(code);

});

変数

中置記法でA = 2にように代入できて

PRINTVAR A

で表示できるようにしてみます。

1や-256のような即値とAやHOGEのような変数との判別が必要となるので、param()を定義します。


var param = function(param){
  var regexp_number = /^-?\d+$/;
  if(regexp_number.test(param)){
    return parseInt(param, 10);
  }else{
    return vars[param];
  }
};

parseInt()の2番目の引数は基数を表します。省略すると自動判別して、「’010′は先頭が0だから8進数だ!」などのように誤判定して10が8になったりと不可解な現象に見舞われるので基数は必ず指定しましょう。


switch(op[1]){
  case '=':
    vars[op[0]] = param(op[2]);
    break;

  default:
    switch(op[0]){
      case 'PRINT':
        console.log(op[1]);
        break;
      case 'PRINTVAR':
        console.log(vars[op[1]]);
        break;
    }
    break;
}

=など中置記法の判定を先に行うようにしました。変数は varsというハッシュに格納されます。


$(document).ready(function(){
  'use strict';

  var evalBasic = function(code){
    var pc = 0;
    var vars = {}; // 変数はここにvars.key = value の形式で登録

    var parse = function(str){
      var lines = str.split("\n"); // 改行で区切る
      var ops = [];
      var i;
      for(i = 0; i < lines.length; i++){
        ops.push(lines[i].split(" ")); // スペースで区切る
      }
      return ops;
    };

    var evalOp = function(op){
      // 即値と変数の判別
      var param = function(param){
        var regexp_number = /^-?\d+$/;
        if(regexp_number.test(param)){
          return parseInt(param, 10);
        }else{
          return vars[param];
        }
      };

      switch(op[1]){
        case '=':
          vars[op[0]] = param(op[2]);
          break;

        default:
          switch(op[0]){
            case 'PRINT':
              console.log(op[1]);
              break;
            case 'PRINTVAR':
              console.log(vars[op[1]]);
              break;
          }
          break;
      }
    };

    var run = function(ops){
      for(; pc < ops.length; pc++){
        evalOp(ops[pc]);
      }
      console.log("END");
    };

    var ops = parse(code);
    run(ops);
  };

  var code = $('#code')[0].innerHTML;
  evalBasic(code);

});

BASICのコード:
A = 55
PRINTVAR A

console:
> 123456789
> END

演算子

いろいろ必要そうな演算子を追加します。
+ – * / % AND OR NOT == < > <= >= <> A != B を A <> B と書けるようにするとBASICっぽい気がします。

このBASICでは全てが文であり、式はありません。パーサが再帰的でないので + を定義しても、A + Bと書けるようになるだけでC = A + B のようには書けないわけです。そういう(a=b+c)文法も作ればいいんのですが、ここではとりあえずA + B の結果を’$1′という変数に格納することにしました。これにより

C = A + B

という意味のことをするには

A + B
C = $1

と書けば良いことになります。


$(document).ready(function(){
  'use strict';

  var f_to_i = function(num){
    return num < 0 ? Math.ceil(num) : Math.floor(num);
  };

  var evalBasic = function(code){
    var pc = 0;
    var vars = {};

    var parse = function(str){
      var lines = str.split("\n"); // 改行で区切る
      var ops = [];
      var i;
      for(i = 0; i < lines.length; i++){
        ops.push(lines[i].split(" ")); // スペースで区切る
      }
      return ops;
    };

    var evalOp = function(op){
      // 即値と変数の判別
      var param = function(param){
        var regexp_number = /^-?\d+$/;
        if(regexp_number.test(param)){
          return parseInt(param, 10);
        }else{
          return vars[param];
        }
      };

      switch(op[1]){
        case '=':
          vars[op[0]] = param(op[2]);
          break;
        case '+':
          vars.$1 = param(op[0]) + param(op[2]);
          break;
        case '-':
          vars.$1 = param(op[0]) - param(op[2]);
          break;
        case '*':
          vars.$1 = param(op[0]) * param(op[2]);
          break;
        case '/':
          vars.$1 = f_to_i(param(op[0]) / param(op[2]));
          break;
        case '%':
          vars.$1 = param(op[0]) % param(op[2]);
          break;
        case 'AND':
          vars.$1 = param(op[0]) && param(op[2]);
          break;
        case 'OR':
          vars.$1 = param(op[0]) || param(op[2]);
          break;
        case '==':
          vars.$1 = param(op[0]) === param(op[2]);
          break;
        case '<>':
          vars.$1 = param(op[0]) !== param(op[2]);
          break;
        case '<':
          vars.$1 = param(op[0]) < param(op[2]);
          break;
        case '<=':
          vars.$1 = param(op[0]) <= param(op[2]);
          break;
        case '>':
          vars.$1 = param(op[0]) > param(op[2]);
          break;
        case '>=':
          vars.$1 = param(op[0]) >= param(op[2]);
          break;

        default:
          switch(op[0]){
            case 'NOT':
              vars.$1 = !param(op[1]);
              break;
            case 'PRINT':
              console.log(op[1]);
              break;
            case 'PRINTVAR':
              console.log(vars[op[1]]);
              break;
          }
          break;
      }
    };

    var run = function(ops){
      for(; pc < ops.length; pc++){
        evalOp(ops[pc]);
      }
      console.log("END");
    };

    var ops = parse(code);
    run(ops);
  };

  var code = $('#code')[0].innerHTML;
  evalBasic(code);

});

ラベル

「*」(アスタリスク)からはじまる行をラベルとします。labelsというハッシュをつくりラベルと行番号の対応を登録しておきます。
初期値としてプログラムの先頭行にHEADというラベルをlabelsに格納しておくことにします。

var labels = {HEAD: 0};

ラベルの読み取りはparse()で行うことにします。


var parse = function(str){
  var lines = str.split("\n");
  var ops = [];
  var i;
  for(i = 0; i < lines.length; i++){
    ops.push(lines[i].split(" "));
    if(ops[i][0] === '*'){ // 追加
      labels[ops[i][1]] = i; // 追加
    }
  }
  return ops;
};

これは、evalOpの中でラベルを読むようにすると、まだ実行されていない行のラベルにGOTOできなくなってしまうからです。

GOTO HOGE // エラー!


* HOGE

GOTO, IF〜THEN GOTO〜

pcの値を変えれば次に実行される行が変わる → GOTOが実装できます。


var label_to_line = function(label){
  if(label === undefined){
    alert("can't goto label: " + label);
  }
  return labels[label] - 1; // あとでpc++される分引いておく
}



case 'GOTO':
  pc = label_to_line(op[1]);
  break;

IFも同様です。IF A THEN GOTO HOGEのような文を想定しているので、THEN, GOTOの部分は無視します。


case 'IF':
  if(param(op[1])){
    pc = label_to_line(op[4]);
  }
  break;

canvas

POINT X Y
でcanvasに点を打てるようにします。
wise9では今までに色々説明されているみたいなので詳細は省きます。

canvasでHello, World!

canvasができたので、ここにドットで文字を書いてみることにします。


// H
X = 1
Y = 1
POINT X Y

Y + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1

Y + 2
Y2 = $1
X + 1
POINT $1 Y2
$1 + 1
POINT $1 Y2

X + 3
X2 = $1
POINT X2 Y
Y + 1
POINT X2 $1
$1 + 1
POINT X2 $1
$1 + 1
POINT X2 $1
$1 + 1
POINT X2 $1

// E
X = 6
Y = 1
POINT X Y
Y + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1

X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y

Y + 2
Y = $1
X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y

Y + 2
Y = $1
X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y

// L
LL = 2
X = 11
* STARTL
Y = 1
POINT X Y
Y + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1

Y + 4
Y = $1
X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y

LL - 1
LL = $1
X = 16
IF LL THEN GOTO STARTL

長いですね、書いていてうんざりしました。これじゃHELLO WORLDじゃなくてHELLです。地獄的なめんどうさ。

せめて「縦線を引く」とか「横線を引く」といった処理をまとめる位のことはしたいですね。サブルーチンを実装してみます。

サブルーチン

サブルーチンとはC言語で言う関数のようなものです。

・GOSUBで指定されたラベルにジャンプして、
・RETURNでジャンプ元に戻り、処理を続行します。

Cの関数との違いは

・変数などのスコープが発生しない(すべてグローバル変数)
・引数を与えられないの2つです。引数が使えないので必要な情報は変数(グローバル変数)で受け渡しします。

スタック構造

サブルーチンの中でサブルーチンを呼んでいったりすると、戻る行を「スタック」で管理する必要があります。幸いjavascriptにはArray#push()とArray#pop()があるのでこれを使いましょう。


var stack = [];

case 'GOSUB':
  stack.push(pc);
  pc = label_to_line(op[1]);
  break;
case 'RETURN':
  pc = stack.pop();
  break;

canvasでHello, World! (再)

だいぶすっきりしたコードになりました。


GOTO MAIN

* TATELINE5DOT
POINT X Y
Y + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
RETURN

* YOKOLINE4DOT
POINT X Y
X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y
RETURN

* MAIN

// H
X = 1
Y = 1
GOSUB TATELINE5DOT
Y = 3
GOSUB YOKOLINE4DOT
X = 4
Y = 1
GOSUB TATELINE5DOT


// E
X = 6
Y = 1
GOSUB TATELINE5DOT
GOSUB YOKOLINE4DOT
Y = 3
GOSUB YOKOLINE4DOT
Y = 5
GOSUB YOKOLINE4DOT

// L
LL = 2
X = 11

* STARTL
Y = 1
GOSUB TATELINE5DOT
Y = 5
GOSUB YOKOLINE4DOT
LL - 1
LL = $1
X = 16
IF LL THEN GOTO STARTL

// O
X = 21
Y = 1
GOSUB TATELINE5DOT
GOSUB YOKOLINE4DOT
Y = 5
GOSUB YOKOLINE4DOT
X = 24
Y = 1
GOSUB TATELINE5DOT

さらなる一般化

引く線の長さが決まっているのはカッコ悪いですね。X, Yに加えて長さLENGTHを指定できるサブルーチンTATELINEを作ってみましょう。

擬似コード

こんな風に書けばいいはずです。


* TATELINE
_Y = Y
I = 0
while(I < LENGTH){
  POINT X _Y
  _Y++
  I++
}
RETURN

whileをGOTOで表現する

「} (閉じかぎカッコ)」はGOTO BEGINLOOPのように置換できます。

while(I < LENGTH){

は少し難しく、
正常ならばGOTOしない→条件分岐を反転したIFでループの終わりにGOTO
なので、こうなります。

_I >= TIMES
IF $1 THEN GOTO TATELINE_LOOP_END

これを使ってHELLOの両脇に線を引いてみます。


GOTO MAIN

* TATELINE5DOT
POINT X Y
Y + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
$1 + 1
POINT X $1
RETURN

* YOKOLINE4DOT
POINT X Y
X + 1
POINT $1 Y
$1 + 1
POINT $1 Y
$1 + 1
POINT $1 Y
RETURN

* TATELINE
_Y = Y
_I = 0
* TATELINE_LOOP_BEGIN
_I >= TIMES
IF $1 THEN GOTO TATELINE_LOOP_END
POINT X _Y
_Y + 1
_Y = $1
_I + 1
_I = $1
GOTO TATELINE_LOOP_BEGIN
* TATELINE_LOOP_END
RETURN


* MAIN

// H
X = 1
Y = 1
GOSUB TATELINE5DOT
Y = 3
GOSUB YOKOLINE4DOT
X = 4
Y = 1
GOSUB TATELINE5DOT


// E
X = 6
Y = 1
GOSUB TATELINE5DOT
GOSUB YOKOLINE4DOT
Y = 3
GOSUB YOKOLINE4DOT
Y = 5
GOSUB YOKOLINE4DOT

// L
LL = 2
X = 11

* STARTL
Y = 1
GOSUB TATELINE5DOT
Y = 5
GOSUB YOKOLINE4DOT
LL - 1
LL = $1
X = 16
IF LL THEN GOTO STARTL

// O
X = 21
Y = 1
GOSUB TATELINE5DOT
GOSUB YOKOLINE4DOT
Y = 5
GOSUB YOKOLINE4DOT
X = 24
Y = 1
GOSUB TATELINE5DOT

X = 0
Y = 0
TIMES = 7
GOSUB TATELINE
X = 25
GOSUB TATELINE

おわりに

言語は「ある程度の妥協」があれば簡単につくることができます。今回は元々シンプルなBASICをさらに簡略化したものをつくりましたが、こんなのでも一応プログラムは書けることがわかりました。BASICすごい言語をのはとても楽しいです。いきなり本格的なものをつくろうとしても挫折するのでこんなかんじの俺言語から始めるといいんじゃないでしょうか。さらに上を目指したい人は「再帰下降構文解析」で検索すると幸せになれます。

#もし面白いと思っていただけたならこのBASICでPONG(テレビテニス)ゲームを作るところまでやります。
文・yayugu

このエントリーをはてなブックマークに追加
はてなブックマーク - [投稿]JavaScriptでBASICっぽい言語をつくる
Post to Google Buzz
Share on GREE

Related posts:

  1. Kinectでキミもアイアンマンになれる!?(実装編)/バイナリとソース付き
  2. かんたんプログラミング #5 関数をもっと呼び出す

Facebook comments:

Post a Comment

Your email is never published nor shared. Required fields are marked *
*
*