Artifact Content
Not logged in

Artifact a076bfb1003e4c62b94f1756ef39c49a44c638d1


Polemy Reference Manual

Polemy Reference Manual

Authors:
k.inaba

License:
NYSL 0.9982 (http://www.kmonos.net/nysl/)

左のサイドバーの "Package" タブをクリックすると実装のソースのドキュメントが読めます。

このファイルは、言語仕様などの、やや辞書的な説明です。
もっとざっくりとした、言語デザインの方向性の魂的なものについては、 「メタプログラミングの会」の発表スライドをご覧下さい。

あと、 やたらとマクロの章が長くなっていますが、 この部分は、 レイヤ機能を入れたら自動的にすごく自然にマクロが入るなーと思って、 おまけで実装してみた程度のものです。 あんまり重要ではないので、適当にスルーして下さいませ。 単に、適当に入れたら適当で微妙な部分が多く残ってしまったので注意書きが増えているだけで…。

Syntax

文法について。 字句解析がわりと適当なので、 変数宣言の変数名のところに、数字を変数名として使えて参照できない変数が作れたり、 予約語は予約語として解釈され得ないところでは普通に変数名として使えちゃったりして、 偶にとんでもない見かけのソースが構文解析通りますが、気にしないで適当に使って下さい。

文字コード

UTF-8 のみ対応です。

コメント

行コメントは # から改行までです。

ブロックコメントはありません。

BNF
 ID    ::= 適当に識別子っぽい文字列
 LAYER ::= "@" ID

 E ::=
   # 変数宣言
     | DECL "=" E (";"|"in") E
     | DECL "(" PARAMS ")" "{" E "}" (";"|"in") E
     | DECL "=" E
     | DECL "(" PARAMS ")" "{" E "}"

         where DECL ::= ("var"|"let"|"def"|LAYER) ID | "@" LAYER

   # リテラル
     | INTEGER                        # 非負整数
     | STRING                         # "" でくくった文字列。\" と \\ は使える
     | "{" ENTRYS "}"                 # テーブル
     | "fun" "(" PARAMS ")" "{" E "}" # 無名関数
     |  "λ" "(" PARAMS ")" "{" E "}" # 無名関数

   # 関数呼び出し
     | E "(" ARGS")"

         where    ARGS ::= E "," ... "," E
                PARAMS ::= (ID|LAYER)+ "," ... "," (ID|LAYER)+
                ENTRYS ::= ID ":" E    "," ... "," ID ":" E

   # 演算子など
     | "(" E ")"                 # ただの括弧
     | "..."                     # これを実行するとdie
     | E BINOP E                 # 二項演算子いろいろ
     | E "."  ID                 # テーブルのフィールドアクセス
     | E ".?" ID                 # テーブルにフィールドがあるか否か
     | E "{" ENTRYS "}"          # テーブル拡張
     | "if" E ("then"|":"|"then" ":") E
     | "if" E ("then"|":"|"then" ":") E "else" ":"? E

   # パターンマッチ
     | "case" E ("when" PATTERN ":" E )* 

         where PATTERN ::= 式がだいたいなんでも書ける気がする

   # レイヤ指定実行
     | LAYER "(" E ")"
糖衣構文

演算子というものはありません。内部的には全て関数呼び出し構文に書き換えられています。if もです。
パターンマッチも全部 if==&&..? を使った関数呼び出し式に書き換えられていますが、 規則の詳細を説明するのが面倒なので適当に想像して下さい。 他の書き換えはこんな感じです。

    if E then E         ⇒ if( E, fun(){E}, fun(){} )
    if E then E else E  ⇒ if( E, fun(){E}, fun(){E} )
    E BINOP E           ⇒ BINOP(E, E)
    { ENTRIES }         ⇒ {}{ ENTRIES }
    {}                  ⇒ {}()
    E {ID:E, ...}       ⇒ .=(E, ID, E) { ... }

変数宣言に色々ありますが、letvardef は同じ扱いで、 in; は同じ扱いです。つまり

   let x = E in E
   var x = E in E
   def x = E in E
   let x = E ; E
   var x = E ; E
   def x = E ; E

以上のどれも同じ意味なので、なんとなく関数型っぽく書きたい気分の日は let in を、 手続き型っぽく書きたい気分の日は var ; を使うとよいと思います。 if then else も微妙にコロンがあったりなかったりバリエーションがありますが好みで使います。

関数を宣言するときは、funλ を省略できます。 以下の書き換えが行われます。

   def f( ARGS ) { E }; E   ⇒   def f = fun(ARGS){E}; E

他に、もっと手続き型っぽくための書き換え色々

   fun () { E; E; E      }   ⇒   fun () { let _ = E in let _ = E in E }
   fun () { var x = 100  }   ⇒   fun () { var x = 100; x }
   fun () { var x = 100; }   ⇒   fun () { var x = 100; x }
   fun () { }                ⇒   fun () { "(empty function body)" }

中身が空の関数に何を返させるかは適当です。今はとりあえず適当に文字列返してます。

変数のスコープ規則

基本的には、let によって常識的な感じに変数のスコープがネストします。

   let x=21 in let x=x+x in x    # 42

一方で、"let rec" のような特別な構文はありませんが、

   let f = fun(x) { if x==0 then 1 else x*f(x-1) } in f(10)  # 3628800

再帰的な関数定義なども、おそらく意図されたとおりに動きます。 内部の詳細は、諸般の事情により、 マジカルで破壊的なスコープ規則になっているのですが、 同名の変数を激しく重ねて使ったりしなければ、 だいたい自然な動きをすると思います、たぶん、はい。

ひとつだけ不可思議な動きをするのは、以下のケースです。

   let x = 1 in
   let f = fun() {x} in
   let x = 2 in
      f()    # 2!!

let-in を縦にチェインしたときだけ、同名変数を破壊的に上書きします (再帰関数の定義が"うまく"いっているのはこの上書きのためです)。 なんでこんなことになっているかというと、 後で説明する「レイヤ」を使ったときに let foo = ... in @lay foo = ... in ... で他レイヤに重ね書きするため、のつもりです。詳しくは後で。

Basic Features

特に特徴的でもない部分を簡単にまとめ。

  • 静的型システムはありません。
  • "ほぼ" 純粋関数型言語です。変数やテーブルのフィールドの破壊的な書き換えはできません。
    ただし、組み込み関数(print)と、変数のスコープ規則のマジカルな片隅に副作用があります。

静的型システムがないのは意図的ですが、破壊的代入がないのは、単に実装がめんどかっただけなので、 今後何か増えるかもしれません。増えないかもしれません。

データ型

以下のデータ型があります。

  • 整数: 0, 123, 456666666666666666666666666666666666666789, ...
  • 文字列: "hello, world!", ...
  • 関数: fun(x){x+1}
  • テーブル: {car: 1, cdr: {car: 2, cdr: {}}}
  • ボトム: (特殊なケースで作られます。「レイヤ」の説明参照のこと。)

関数はいわゆる「クロージャ」です。静的スコープで外側の環境にアクセスできます。 テーブルはいわゆるプロトタイプチェーンを持っていて、 自分にないフィールドの場合は親に問い合わせが行く感じになっていますが、 フィールドの書き換えがないので、これは特に意味ないかもしれない…。

また、リストを扱うために、いわゆる「cons リスト」を使います。 空リストを {}、1個以上要素があるものを {car: 先頭要素, cdr: 二番目以降のリスト} という形で。この形でリストを扱わなければならないという決まりはありませんが、 この形は特別扱いされて print で綺麗に出力されたりします。

パターンマッチ

適当に実装されたパターンマッチがあります。 リストの 2n 番目と 2n+1 番目を足して長さを半分にする関数:

    def adjSum(lst)
    {
      case lst
        when {car:x, cdr:{car: y, cdr:z}}: {car: x+y, cdr: adjSum(z)}
        when {car:x, cdr:{}}: lst
        when {}: {}
    }

動かすときには、処理系がそれっぽい if-then-else に展開しています。 when を上から試していって、最初にマッチしたところを実行します。 どれにもマッチしないとエラーでプログラム終了します。

   PAT ::= "_"                                      # ワイルドカード
         | ID                                       # 変数パターン
         | "{" ID ":" PAT "," ... "," ID : PAT "}"  # テーブルパターン
         | E                                        # 値パターン

変数パターンは常にマッチして、値をその変数に束縛します。 ワイルドカードも常にマッチしますが、変数束縛しません。 値パターンは、任意の式が書けます。その式を評価した結果と == ならマッチします。 外で束縛された変数を値パターンとして配置、は直接はできないので

   var x = 123;
   case foo
     when {val: x+0}: ... # これは {val:123} と同じ
     when {val: x}:   ... # これは任意の foo.?val なら常にマッチ

適当にちょっと複雑な式にしてやるとよいかも(裏技)。

テーブルパターンは、書かれたキーが全てあればマッチします。 {a: _} は、.a を持ってさえいればマッチするので、 {a: 123, b: 456} なんかにもマッチします。 なので、リストに対するパターンを書くときには、car/cdr の場合を先に書かないと when {} を上に書くと全部マッチしちゃいます。注意。

Layers
[Layers :: Overview]

  Polemy's runtime environment has many "layer"s.
  Usual execution run in the @value layer.

    >> 1 + 2
    3
    >> @value( 1 + 2 )
    3

  Here you can see that @LayerName( Expression ) executes the inner Expression in
  the @LayerName layer. Other than @value, one other predefined layer exists: @macro.

    >> @macro( 1+2 )
    {pos@value:{lineno@value:3, column@value:9, filename@value:},
      is@value:app,
    args@value:{car@value:{pos@value:{lineno@value:3, column@value:9, filename@value:},
                            is@value:int,
                          data@value:1},
                cdr@value:{
                  car@value:{pos@value:{lineno@value:3, column@value:11, filename@value:},
                              is@value:int,
                            data@value:2},
                  cdr@value:{}}},
     fun@value:{pos@value:{lineno@value:3, column@value:10, filename@value:},
                 is@value:var,
               name@value:+}}

  (Sorry, this pretty printing is not available on the actual interpreter...)
  This evaluates the expression 1+2 in the @macro layer. In this layer, the meaning of
  the program is its abstract syntax tree.

  You can interleave layers.
  The root node of the abstract syntax tree is function "app"lication.

    >> @value(@macro( 1+2 ).is)
    app



[Layers :: Defining a new layer]

  To define a new layer, you should first tell how to "lift" existing values two the new layer.
  Let us define the "@type" layer, where the meaning of programs is their static type.

    >> @@type = fun(x) {
    >>   if( _isint(x) ) { "int" } else {
    >>   if( _isfun(x) ) { x } else { "unknown" } }
    >> }
    (Note: polemy REPL may warn some exception here but please ignore)

  For simplicity, I here deal only with integers.
  _isint is a primitive function of Polemy that checks the dynamic type of a value.
  For function, leaving it untouched works well for almost all layers.

    >> @type( 1 )
    int
    >> @type( 2 )
    int
    >> @type( "foo" )
    unknown

  Fine! Let's try to type 1+2.

    >> @type( 1 + 2 )
    ...\value.d(119): [:6:8] only @value layer can call native function

  Note that the behavior of this program is
    - run 1+2 in the @type layer
  and NOT
    - run 1+2 in @value and obtain 3 and run 3 in the @type.
  The problem is, the variable "+" is defined only in the @value layer.
  To carry out computation in the @type layer. We need to define it also
  in the @type layer.

  To define some variable in a specific layer, use @LayerName in place of
  (let|var|def)s.

    >> let x = 2
    >> @value x = 2
    >> @type x = "int"
    >> @hoge x = "fuga"

  For "+", do it like this.

    >> @type "+" = fun(x,y) {@value(
    >>   if( @type(x)=="int" && @type(y)=="int" ) { "int" } else { "typeerror" }
    >> )}
    polemy.value.native!(IntValue,IntValue,IntValue).native.__anonclass24

  It is just computing the return type from the input type.
  Not here that the intended "meaning" of if-then-else is the runtime-branching,
  and the meaning of "==" is the value-comparison. These are the @value layer
  behavior. So we have defined the function body inside @value layer.
  But when we refer the variables x and y, we need its @type layer meaning.
  Hence we use @type() there.

  Now we get it.

    >> @type( 1 + 2 )
    int

  Well, but do we have to define the @type layer meaning for every variables???
  No. After you defined @type "+", you'll automatically get the following:

    >> def double(x) { x + x }
    (function:17e4740:1789720)

    >> @type( double(123) )
    int

  Every user-defined functions are automatically "lift"ed to the appropriate layer.
  Only primitive functions like "+" requires @yourNewLayer annotation.



[Layers :: neutral-layer]

  let|var|def is to define a variable in the "current" layer.
  Not necessary to the @value layer.

    >> @value( let x = 1 in @value(x) )
    1

    >> @macro( let x = 1 in @value(x) )
    polemy.failure.RuntimeException: [:14:29] variable x not found

    >> @macro( let x = 1 in @macro(x) )
    {pos@value:{lineno@value:15, ...



[Layers :: Layered-Parameters]

    >> def foo(x @macro @value) { {fst: x, snd: @macro(x)} }
    (function:1730360:1789720)

  If you annotate function parameters by @LayerNames, when you invoke the function...

    >> foo(1+2)
    {snd@value: {pos@value:{lineno@value:17, column@value:5, filename@value:},
                  is@value:app, arg@value:{...
    /fst@value:3
    /}

  its corresponding arguments are evaluated in the layer and passed to it.
  If you specify multiple layers, the argument expression is run multiple times.
  If you do not specify any layer for a parameter, it works in the neutral layer.
Macro Layers

Polemy 言語組み込みのレイヤは @value@macro の二つです。 (内部的にはもういくつかありますが、ユーザから直接は使えません。) @value は、「普通に」普通のセマンティクスでプログラムを実行するレイヤでした。 @macro は、実は、@value よりも前に実行されるレイヤで、 「プログラムを実行するとその構文木を返す」というセマンティクスで動きます。

    (ここに例)

動きとしてはこうです。

  1. 関数呼び出し時(とトップレベル環境の実行開始時)に、 まず、@macro レイヤでコードを実行。
  2. 返ってきた構文木を、その関数を呼び出したときのレイヤで実行。

@macro レイヤも所詮ただのレイヤですので、 上で説明した方法で @macro レイヤに関数などを登録しておくことで、 構文木の生成をいじることが可能です。まさにマクロ。

概要

samples/macro.pmy にいくつか使い方サンプルが置いてありますので、詳しくはそちらをどうぞ。

    >> @macro( twice(print("Hello")) )
    {
      pos: {lineno:1, column:9, filename:},
     args: [ { pos: {lineno:1, column:15, filename:},
              args: [{pos:{lineno:1, column:21, filename:},
                       is:Str,
                     data:Hello}],
                is: App,
               fun: {pos:{lineno:1, column:15, filename:}, is:Var, name:print}}
           ],
       is: App,
      fun: {pos:{lineno:1, column:9, filename:}, is:Var, name:twice}
    }

詳細は気にしなくて構いませんが、とにかく、@macro レイヤでは、 基本的には、コードを実行するとそのコードの構文木がでてきます。 この挙動は @macro レイヤの変数をセットすることで、カスタマイズできます。

    >> @macro twice(x) { x; x } in twice(print("Hello"))
    Hello
    Hello
    Hello

(3回出力されてますが、3個目は print(x) の返値は x なので、 それがREPLによって印字されているだけです。) @macro レイヤで in 以降を実行すると、print("Hello") という式を表す構文木が作られ、 それが twice 関数に渡されます。twice の中身も @macro レイヤで実行されるので、 構文木を作ろうとしますが、変数 x には @macro レイヤで値が入っているので、 その値を読み取って構文木を作成します。 結果として、2回 print("Hello") する構文木が作られて、 その後で、それが @value レイヤで実行されています。

本当にベタに構文木を作るだけなので、変数名の衝突などなどは気にしません。「衛生的でない」マクロです。

    @macro LetItBe(x, y) { var it = x; y };  # y の中で変数 it が使える
    print( LetItBe("myself",  "when I find " ~ it ~ " in times of trouble") );

変数名に気をつけるには、組み込み関数 gensym() を使って頑張って下さい。

レイヤ切り替え

他のレイヤ同様、@macro レイヤを実行中に @layer( ... ) 構文を使うことで、 別のレイヤでコードを動かすこともできます。よく使う例は、@value レイヤに移ることで構文木を普通に計算して色々プログラム的にいじる用途です。

    @macro reverseArgs(e) {@value(
        def rev(xs, acc) {
          case xs when {car:x, cdr:xs}: rev(xs, {car:x, cdr:acc}) when {}: acc
        };
        case @macro(e)
          when {is:"App", fun:f, args:as}: {is:"App", fun:f, args:rev(as,{})}
          when e: e
    )};
    print( reverseArgs(1-2) ); # 2-1 == 1

reverseArgs は、関数呼び出しの構文木の、引数の順番を逆転する関数です。 @macro(e) によってマクロレイヤにセットされている構文木引数を取り出し、 それを @value レイヤによる普通の計算プログラムで操作しています。 @macro(...) はいわゆる「準クオート (quasiquote)」、 @value(...) は「逆クオート (unquote)」にちょっと近いかもしれません。

@layer(...) だけでなく、関数のレイヤ指定引数なども同様に使うことができるので、 一部の引数は @macro、一部の引数は @value レイヤで受け取る関数を書くなど、 さらに色々面白いことが可能です。

構文木の構造

構文木がどのようなテーブルで渡されてくるかについては、ソースドキュメントの polemy.ast のページをご覧下さい。例えば変数名を表す Var クラスには、 継承の分も合わせて LexPosition pos;string name; の2つのメンバがあるので

    { is:   "Var",
      pos:  {filename:"foo.pmy", lineno:123, column:45},
      name: "x" }

こんな感じのテーブルになります。 クラス名が is フィールドに、メンバ変数はそのままの名前で入ります。 配列メンバは cons リストになって入ってきます。 自分で構文木を作る時は、pos フィールドだけは省略しても構いません。

微妙なところ1

ここまで、@macro が本当にただの1レイヤと説明してきましたが、 実はちょっとトリックが潜んでいます。

    >> @macro twice(x) {x; x} in twice(@value(print("Hello")))
    Hello
    Hello
    Hello

先ほどの例に @value を増やしたものですが、これでもやはり、Hello が2回 print されるようになります。これは本来はおかしな話で、print("Hello")@value レイヤで実行されて値に落ちるはずなので、1回しか print されないはず。

実は、Polemy の中では、@macro レイヤと (rawmacro) レイヤという二つの異なるマクロ用レイヤが動いています。

  • (rawmacro)@macro も、コードを動かすとその構文木を返す意味論。
  • ただし、(rawmacro)@macro も、 @macro レイヤに値のセットされた変数をみつけたときは、 その変数という構文木を作るのではなく、変数の内容を展開。
  • また @macro は、 レイヤ指定式を見ると実行レイヤを切り替て、構文木生成モードをやめてしまう。
  • (rawmacro) は、 レイヤ指定式を見ても実行レイヤを切り替えないで構文木にする。

ユーザーから直接 (rawmacro) は呼べませんが、 「関数やトップレベル実行開始前のマクロ処理は (rawmacro) で実行開始」 「@macro レイヤ以外で呼び出した関数の仮引数に @macro がついていたら、 その実引数は (rawmacro) で実行」 という2つのタイミングで (rawmacro) が動き出します。 (rawmacro)@macro レイヤから変数を見つけてマクロし始める時に、 そこで @macro に動作が移ります。

こうなっているのは、全部がレイヤ指定式に反応する @macro の動作だと、 レイヤを使ったプログラムが全て @value 実行時ではなく、 マクロ展開の時点で動き始めてしまって、おかしなことになるためです。 色々考えた結果、とりあえずこの中途半端な混合が具合がよいのではないかということになりました。

微妙なところ2

「関数実行開始時に、まずマクロレイヤを実行」と書きましたが、この時、関数内関数まで辿りにいくので、 何重にもネストした関数を使っていると、内側の関数は、何重にもマクロ展開が走ってしまいます。 これはなにかおかしい気がしますね。Scheme などはどうなっているのか調べないと…。

微妙なところ3

これはエラーになります。

    >> let _ = (@macro twice(x) {x;x} in twice(print("Hello")))
    polemy.failure.RuntimeException@C:\Develop\Projects\Polemy\polemy\value.d(109):
    [:2:35] 'twice' is not set in @value layer

どういうことかというと、@macro で定義したマクロはいつから使えるようになるかという話で、 この @macro twice(x) {x;x} in ... の部分は @value レイヤの式なので、 まずこの式全体のマクロ展開が終わったあとにしか実行されないのです。twice がマクロと見なされはじめるのは、@macro 実行が終わった後。 なので、 例えば twice(print("Hello")) の部分を無名関数にラップしてやれば、 マクロ展開を遅らせられて、 ちゃんと実行ができます。

これだと余りにも不便なので、関数のトップレベルの変数宣言式の列についてだけは、 @macro@value の評価を交互にインターリーブするようにしました。 「関数やREPLのトップレベルの最初に宣言したマクロだけは、その関数内で即座に使える」わけです。 これも Scheme の let-syntax などなどの動きを調べて勉強しないと…。

Built-in Primitives

組み込み関数・変数の一覧。

テーブル操作
{} () 空のテーブルを作る
. (t, s) テーブル t の名前 s のフィールドの値を取得。なければ undefined
.? (t, s) テーブル t に名前 s のフィールドがあれば 1、なければ 0
.= (t, s, v) テーブル t を親に持ち、名前 s のフィールドに v が入ったテーブルを作る

制御フロー
if (n, ft, fe) n が非 0 なら ft()、0 なら fe() を実行

演算
+ (n, m) 整数 n と整数 m を足して返す
- (n, m) 整数の引き算
* (n, m) 整数の掛け算
/ (n, m) 整数の割り算
% (n, m) 整数の剰余
&& (n, m) 整数 n と m が両方非 0 なら 1、それ以外では 0
|| (n, m) 整数 n と m がどちらか非 0 なら 1、それ以外では 0
~ (a, b) a と b を文字列化して結合
< (a, b) a と b を比較
<= (a, b) a と b を比較
> (a, b) a と b を比較
>= (a, b) a と b を比較
== (a, b) a と b を比較
!= (a, b) a と b を比較

注意点として、作者の趣味の問題で、&&|| は short-circuit 評価をしません。 整数演算の種類が少ないのは、D 言語の std.bigint がビット演算などをサポートしてないためです。 文字列が結合しかできないのは、単に手抜きです。

外部とのやりとり
print (a) a を文字列化標準出力に改行付きで表示して、a を返す
argv スクリプトに渡された引数文字列のconsリスト
gensym () エセgensym。変数名として他とかぶらなそうな文字列を返します
rand (n) 0 以上 n 未満の自然数を31bit以内でランダムに生成します

データ型判定
_isint (a) a が整数なら 1、でなければ 0
_isstr (a) a が文字列なら 1、でなければ 0
_isfun (a) a が関数なら 1、でなければ 0
_istable (a) a がテーブルなら 1、でなければ 0
_isundefined (a) a が未定義値なら 1、でなければ 0
Page was generated with on Sat Nov 27 01:54:49 2010