プログラミング言語 Essen

  • (by K, 2016.06.29)

これはなに?

  • persistent-C(KHPC)を作っているうちに、C言語をベースとして言語拡張するのには限界を感じたので、新しいベース言語がほしくなった。それでかなり柔軟な文法仕様を考えた。これをひとまず「nv系言語仕様」と呼ぶことにする。Essenは、このnv系言語仕様をベースにKが開発しているプログラミング言語。
  • nv系言語仕様は、構文仕様の機能仕様が分かれている。
  • これが完璧にして最高の仕様だとは思っていないので、もしこれより良い仕様を教えてもらえたらそれを真似する可能性はあります。
  • インタプリタを想定していますが、インタプリタじゃなければダメだとは思っていません。
  • 実行速度はあまり追及していません。どうせインタプリタなのでそこを追及してもしょうがないかなと。
  • 柔軟にいろんな既存言語に似た書き方ができて、その割に処理系の実装が単純になるように考えています。
  • もし速度がほしくなったら、その時に仕様を見直して、作り直せばいいと思っています。
  • 字句解析のルールは軟弱なので、「+-3」とか「a+++++b」みたいなケースで、C言語とは違う切り分けになります。でもこんなのレアケースだし、そういう時はスペースを入れたほうが人間にも読みやすいと思うので、以下の字句解析ルールはシンプルな割には優秀だと思っています。

構文仕様

  • 字句解析のルール:
    • 字句解析というのは、ソースコードをトークンに切り分けること。
    • 文字列定数の中以外で、//が来たら行末まで無視する。
    • 文字列定数の中以外で、/* */があったらそれはスペース1個と同等にふるまう。
    • /* ~ */ は入れ子にできる。
    • 文字定数や文字列定数は切り分けない(当然である)。
    • CRとLFとスペースとタブは文字種0とする。「!%&-=^~|+*:.<>/」を文字種2とする。「(){}[],;」を文字種3とする。そしてそれ以外を文字種1とする。ただしこの文字種区分は処理系によって多少異なってもよい。
    • 文字種0は必ずトークンの切れ目にはなるが、トークンの一部にはなれない。
    • 文字種1は文字種1がつながっている限り、トークンの切れ目にはならない。それ以外の文字種が来たらそこがトークンの切れ目になる。
    • 文字種2は文字種2がつながっている限り、トークンの切れ目にはならない。それ以外の文字種が来たらそこがトークンの切れ目になる。
    • 文字種3は何が後続しようとも、1文字だけでトークンを構成する。
    • 唯一の例外として、数字で始まるトークン内に.があった場合は、これを切り分けない(小数点として扱いたいため)。
  • 構文解析のルール:
    • カッコの対応関係に注意しつつ、文末の;を探す。なお、中かっこ閉じ「}」の直前の;は省略可能なので、それにも配慮する。
    • 文の中のトークンを一つずつ確認する。もし演算子属性のトークンがあれば、演算子に対して「テスト」を実施する(マッチング・テスト)。演算子は、テスト用の関数があらかじめ定義されていて、そこでは演算子の前後の要素を確認する。その結果から、演算子として発動しうるかどうかを決定する。
    • 文の中に発動可能な演算子が複数ある場合は、最も高い優先度のものが選ばれる。同じ優先度のものが複数あった場合は、左結合型演算子の場合は最も左の物、右結合演算子の場合はもっとも右の物が選ばれる。同じ優先順位で左結合演算子と右結合演算子が混在している場合、それはエラーになるべきである。
      • ちなみにEssenでは優先順位に整数を採用し、優先順位が偶数なら左結合で、奇数なら右結合としている。
    • こうしてどの演算子が発動するか決まったら、その演算子は実行され、その結果でトークン列は一部置換される。
    • そして残ったトークン列に対して、上記に戻ってまた演算子トークンを探す。そして発動可能な演算子トークンが一つもなくなったら、次の文へ進む。
    • なお、中かっこでくくった部分については、演算子は一切発動しない。
    • 何が演算子で何が演算子でないかは、nv系言語仕様では規定されない。それはユーザが自由に決めてそれを守ればいいと考える。

構文構成例

  • 普通の計算。
    a=b+c*2;
    • 「 a = b + c * 2 ; 」
    • 最初に二項演算子の * が発動して、c*2の結果と置換される。ここでは6だとしよう。
    • 「 a = b + 6 ; 」
    • 次に二項演算子の + が発動して、b+6の結果と置換される。ここでは8だとしよう。
    • 「 a = 8 ; 」
    • 最後に二項演算子の = が発動して、aに8が代入され、式の値8に置換される。
    • 「 8 ; 」
    • もう演算子はないので、次の文に制御が移る。
  • 単項演算子と二項演算子が混ざった例。
    a = -1 * +1;
    • 「 a = - 1 * + 1 ; 」
    • 単項演算子の+や-は、二項演算子の*よりも優先度を高く設定しておいたとする。
    • 単項演算子の+や-は、自分の後ろに数か数になりうるものがあって、かつ自分の前に数か数になりうるものがない場合にのみ発動する。これを満たさなければ二項演算子かもしれない。
    • このルールにより、aには正しく-1が代入されることになる。
  • 二重のマイナス符号の例。
    a = - -b;
    • 「 a = - - b ; 」
    • 仮に単項マイナスが左結合だったとしても、左の-は、最初は発動できない。なぜなら演算子の後ろに数がなく、数になりえない演算子が来ているから。ということで、 - b が評価されて -2 が与えられる。
    • 「 a = - -2 ; 」
    • あとは普通に処理されて、aは2になる。
  • 英字で構成された演算子。記号で構成された変数名。
    • 「mod」や「and」などの英字で構成されたものも、nv系言語処理系では演算子になりうる。逆に「+++」という名前の変数があってもよい。スペースで区切れば問題なく使える。
    • 「sin」を演算子としておけば、「y=sin x;」なども可能である(カッコがいらない、あってもいいけど)。
  • 代入方向の逆転。
    • 「=>」という演算子を定義しておけば、「a + b => c;」という記述も可能である。
  • 逆ポーランド記法。
    • 「<*>」や「<=>」という逆ポーランド記法の演算子があり(積と代入)、これが左結合で前方2つの項に対して作用するとすれば、「a b c <*> <=>;」という表記が可能である。
    • また混在も可能である。「a = b c <*>;」
  • 制御構文について。
    • たとえば「if」は関数である。条件が成立した時の { } や不成立の場合の { } は、たとえ中に1文しかなくても中カッコを省略できない。これらの { } はif関数に対する引数として扱われる。区切りのカンマは特には必要ない。関数名やその後ろの ( ) に後続する中かっこはすべて引数のコード片とみなされる。
      if (a == 3) { b = a; }; // 最後の;は必要で省略できない。
      if (a < b) then { max = b } else { max = a }; // thenやelseを演算子として設定して、さらに無条件に自身が消えるだけにすれば、こういう書き方も可能なはず。
      max = if (a < b) { b } { a }; // ifが値を返すとしたら、この書き方も許せるはず。
  • for文について。
    • forをそれっぽく書くのには限界がある。「for { i = 0 } { i < 10 } { i++ } { ... }; 」この書き方で良ければ、forを関数とみなせる。
    • 「while { 条件式 } { ... }; 」は問題ない。
    • 「do { ... } while { 条件式 }; 」は、doを演算子にして、後続するwhileをdo演算子によって消し去ってしまえば、可能。
    • いずれにせよ、何度も評価する条件式は必ず中カッコでくくってコード片にしておく必要がある。
      • もしくはdoやwhile演算子が、後続するカッコを中かっこに置換するという方法も考えられる。この場合、doやwhileはカッコよりも高い優先順位を与えておかなければならない。
  • 中かっこでくくられたコード片について。
    • これらが関数に渡されるときは、単なる文字列としてソースコード片が渡される。evalなどを使って実行することになる。最後の式の値が「evalの値になる」という仕様を想定している。
  • ほとんどすべての演算子や構文のための関数が、ユーザ関数として記述されていることが望ましい。そうすれば、処理系をいじらずとも自由に構文を変更できる。
    • 優先順位などのつじつまを考えるのは容易ではないかもしれないが、それでも処理系を改造しなければ演算子や構文を変更・追加できないよりはずっといい。
  • ifについて。
    • if (条件式1) { 文1 } { 条件式2 } { 文2 } { 条件式3 } { 文3 } ... { 文e };
    • つまりif関数は、引数の数が可変である。中カッコでくくられたコード片が1つの場合、文1しかないと分かる。コード片が2個の場合は、文1と文eがあると分かる。コード片が3個の場合は、文1と条件式2と文2があると分かる。以下略。
    • これは else if が連なったものを表していて、最後の文eはelse節に相当する。

機能仕様

  • 整数型は256ビットの精度を持つ。実数型は8倍精度。
  • 変数には型がなく、値が型を持つ。ヌル型、整数型、実数型、構造体型、ポインタ型が基本型になる。
  • 全ての変数には「リビジョン」という属性があり、代入するたびに値が増加する。これにより値が変更されたかどうかを判定しやすくなる。リビジョンも256ビットの整数。
    • 構造体に関しては、メンバ変数が更新されるたびに自身のリビジョンも更新される。デフォルトでは、どんどん親に波及するが、波及させない属性が設定されていればそこで止まる。
  • 構造体型は、メンバ名をabc.memberと書いてもよいし、abc["member"]と書いてもよい。つまり構造体は配列の代用もできる(メンバ名は整数値でもよい。ただしその場合は、ピリオドによる表記は使えない)。

おまけ

  • 関数の引数として { } のコード片を渡せるという仕様は、Rubyの「ブロック付きメソッド呼び出し」がうらやましくて真似をしたのがきっかけ。

こめんと欄


コメントお名前NameLink

トップ   編集 凍結 差分 バックアップ 添付 複製 名前変更 リロード   新規 一覧 単語検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2016-10-24 (月) 18:19:43 (1114d)