公開技術情報

[English] [Japanese]

A. LuneScript の言語仕様でミスだったと思っているところ

LuneScript は、自分の中で初の本格的なプログラミング言語開発の経験でした。

自分なりにさまざまな言語を調べ、 できるだけ使い勝手の良い言語になるよう設計をしてきましたが、 それでも「ここは良くなかった」と思う点は幾つかあります。

ここではそれらの点を挙げて、どうするべきだったかをまとめます。

LuneScript はトランスコンパイラなので、一般的なプログラミング言語としての話だけでなく、 トランスコンパイラ特有の話も挙げます。

基本的な記号(?)を、あまり重要でない機能に使ってしまった

LuneScript は、ある文字の文字コードを取得する際に、 ? を使います。

例えば a の文字コード 0x61 を取得するには、以下のように書きます。

// @lnsFront: ok
let code = ?a; // (0x61)

簡単に文字コードを取得できる、という意味では良いのですが、 「文字コードを取得する」という制御が必要になるケースは、 一般的なプログラミングにはほとんどありません。

そのほとんど使用しないケースに ? を使ってしまったため、 別の言語機能に記号を紐付けたいときに ? が使えなくなってしまいました。

ASCII コードで利用可能な記号において、

  • パッと見の認識しやすい文字
  • キーボードからタイプしやすい文字
  • 他のプログラミング言語の基本的な制御で利用されていない文字

上記の条件を全て満す記号は次しかありません。 (人によって多少増減すると思います)

# $ ? \ & @ !

この貴重な記号を、ほとんど使わない機能に割り当ててしまったのは大きな失敗でした。

特に「 ? の直後の文字のコードを取得する」という仕様では、 ==>= などの記号の組み合わせの中に ? を含めて使用することも出来ません。

Parse に影響するような仕様(immediate な文字列表現、数値表現、etc…) をプログラミング言語に取り込む際は、 十分に注意が必要でした。

トランスコンパイル先の Lua に影響を受け過ぎた

当初の LuneScript は、Lua 専用のトランスコンパイラでした。

当然 LuneScript の想定ユーザは、 Lua プログラマです。

よって、LuneScript は Lua のプログラマが扱い易いように言語仕様を定義しています。

これには次のメリットがあります。

  • LuneScript の学習コストが低い
  • LuneScript で書いたコードを Lua へ変換する際に、 多くの場合そのままの形で変換でき、変換後のコードを追い易い

しかし、次のものに関しては、Lua とは異なる仕様にするべきでした。

  • List などのインデックスが 0 からではなく 1 から始まる
  • ~=.. などの独特な演算子
  • and or の演算結果
  • 多値返却の動作
  • 組込み関数仕様

この中で、最初の「インデックス」と、「 ~=.. などの独特の演算子」に関しては、 他の一般的なプログラム言語との仕様が違うので使い分けしないとならない、 という使い勝手の問題です。

一方で、残りは Lua 以外へのトランスコンパイル処理を考えた時に処理が複雑になってしまう、 という問題です。 処理が複雑になることで、変換後のコードを追い難くなるのと、 変換後のコードの実行時にオーバーヘッドが掛る、ということにつながります。

「and or の演算結果」と「多値返却の動作」の仕様は、 Lua の VM がスタックマシンであることが前提の仕様なので、 多くのプログラミング言語はスタックマシンを前提としていないため、相性が悪いです。

そして、「組込み関数仕様」に関しては、 仕様をもう少し簡易化したり、抽象度を上げて設計するできでした。 特に Lua の文字列パターンマッチ系の処理は、かなりの Lua 独自仕様なので、 これを別の言語で再現するのは非常にオーバーヘッドが掛ります。

go では、Lua の文字列パターンマッチ系の処理を自前で再現するのは大変だったので、 liblua あるいは gopherlua を利用して処理させています。 これが影響して、 go にトランスコンパイルした go コードに Lua のランタイムが 必要になってしまっています。

トランスコンパイラとはいえ一つの独立したプログラム言語であるので、 変換先の言語を意識し過ぎずに、自分が理想とする言語仕様を検討するべきでした。

immutable のデフォルト

変数やメンバなどの型を宣言する際、 & を型名の前に付けると immutable を示します。 そして、 & を付けずに型名だけの場合は mutable です。

let val:&List<int> = [ 1, 2 ];  // immutable list
let val:List<int> = [ 1, 2 ];   // mutable list

一方で、変数宣言で型推論を使った初期化を行なうと、 mut を変数名の前に付けるとその変数と型が mutable になります。 そして、 mut を付けないと変数と型が immutable になります。

let mut val = [ 1, 2 ];  // mutable list
let val = [ 1, 2 ];   // immutable list

修飾子の有無によって型の mutable が変わるのですが、 mut と & とで mutable の動作が逆になってしまっています。

これは、非常に紛らわしいです。

何故こうなってしまったかというと、 当初は Rust のような所有権制御を実現したかったんですが、 所有権制御を実現する前にトランスコンパイラとして動かすことを優先したため、 型の制御を中途半端なまま実装をしてしまい、 その動作が今も残っている、という状況です。

mutable 制御は、現状の仕様のまま残すしかないですが、 所有権制御は何らかの形で実現したいと思っています。

デカい言語仕様

Lua は何気に言語仕様がデカいです。

増改築を続けまくっている古の言語に比べれば全然少ないですが、 generics 対応していないころの Go と同程度くらいの仕様のボリュームはあります。

そのくらいなら少ない方だ、と言われるかもですが、 基本一人で全てをメンテしている言語ということを考えると、結構な規模です。

さらに LuneScript のフルスペック、を考えると、 Lua のランタイムを内包するという、なかなかに無茶な仕様もあるので、 もしかしたらそこらの言語よりも言語仕様が大きかもしれません。

この仕様の大きさ故、 新しくトランスコンパイル先の言語を追加する、となると、 非常に時間がかかります。

実際、C 言語へのトランスコンパイラ対応をしていましたが、 仕様が大き過ぎて途中でモチベが尽きる、という悲しいことになりました。

新しい言語仕様を考えるのは楽しいですが、 それを複数の言語で実現する手間を考えると、 例え自分しか使っていない言語だとしても、 言語仕様はコンパクトで効果があるもの、 を意識するべきだと改めて思いました。

これが多くの人が使う言語なら、なおさらでしょう。

トランスコンパイラ先に新しい言語を追加する場合、 以下が揃っているのと、いないのとでは、だいぶ作業の工数が変ってきます。

  • gc 対応の言語である
  • class/interface が実現可能である
  • 多値返却を扱える
  • string/list/set/map を扱える
  • クロージャをサポートしている
  • シンボルのスコープがレキシカルで、ファイル内スコープがある。
  • Lua を扱える
  • 何でもアリな型との相互キャストが可能
  • コンパイルエラーが厳し過ぎない

「多値返却を扱える」「Lua を扱える」以外は、 イマドキの新しいスクリプト系言語なら大丈夫そうな気がします。

なお、「Lua を扱える」は、次の場合には問題になりません。

  • トランスコンパイル元のコード内に、動的に Lua を扱うコードを書かない
  • 文字列のパターン処理を、その言語向けに独自実装する

なお、トランスコンパイラ可能な言語を増やすには、 AST からその言語のコードを生成する処理を書けば良いだけなので、 それほど LuneScript の内部処理の知識がなくても開発できます。

もしも興味があれば、挑戦してみてください。

マクロ内で LuneScript が使える

LuneScript のマクロ内で LuneScript のコードを書けます。

これ、そんなに難しくないように思うかもですが、 結構面倒なことをやってます。

コンパイル時に、マクロに書かれているコードを解析し、 それを実行して出力結果をコンパイルする。ということをやっている訳です。

ちょっと、コレ頑張りぎたな。と反省しています。

しかも、マクロの残念なところとして、 マクロの処理は並列処理が出来ない、ということが挙げられます。。。

よって、マクロを多用していると、マクロを使っていないケースと比較して、 ビルド時間が掛ってしまう傾向にあります。

イマドキではない仕様

LuneScript にはイマドキではない仕様がいくつかありますが、 それらは敢えてそうしています。

  • 文の区切りに ; が必要
  • シンボルのアクセス制限

比較的新しい言語では ; がない方が主流なような気がしますが、 ; があった方が確実に文が終っていることが、 ぱっと見で、分かり易いと考えているので、あえて必須にしています。

また、シンボルのアクセス制限をシンボル名の Prefix で変更する言語もありますが、 シンボル名は自由に付けられた方が自然だろう。 というのが個人的な考えです。

さいごに

いろいろとミスったところを挙げましたが、 これらを経験できたのも、 本格的なプログラミング言語開発を実際に行なったから得られた知見です。

もちろん、書籍やこのようなネットの記事で情報を得ることは出来ますが、 自分で経験するのと、読んで判ったつもりになるとでは全く違います。

「愚者は経験に学び、賢者は歴史に学ぶ」と言いますが、 可能な範囲で経験できることは経験した方が良いです。

「本格的なプログラミング言語開発」をしたこと自体は、 人から愚者といわれようが間違いなく非常に有意義だったと考えています。