U-22プログラミングコンテストで経済産業大臣賞＜総合＞に輝いたとウワサのBlawn言語を触ってみたのでそのメモです

GitHubリポジトリはこちら

https://github.com/Naotonosato/Blawn

動かし方

簡単には動かないのでいくつかPRが出てますが、素のままのリポジトリをcloneしてきたなら以下の手順で動かせるようです。

$ chmod +x Blawn/blawn $ chmod +x Blawn/data/llc $ mkdir Blawn/tmp

バイナリはLinux向けのもののみ同梱されています。

Hello World

シンプルに print だけでできます。

sample/hello.blawn print("hello")

コンパイルは以下のコマンド。実行まで一緒にやってくれるようです。

$ ./Blawn/blawn sample/hello.blawn hello

コンパイル後はバイナリが残るのでそのバイナリを実行もできます。

$ ls Blawn Blawn.code-workspace LICENSE README.md hello.out sample src $ ./hello.out hello

どんな言語なの

独自の系統の文法を持つシステムプログラミング言語のようです。コンパイラのツールスタックはflex, bison, LLVMと定番のツールを使い熟して書かれています。

Blawnの特徴は

型名の記述が一切不要

構文の可読性が高い

すべての関数/クラスがC++でいうところのテンプレート関数/クラス

コンパイル速度と実行速度が速い

メモリが安全

とのこと。

型の記述が不要なのは型推論に因るものですが、型推論を知らない人には何をやってるのか分からないようで、混乱を生んでいますね。

下記の記事なども参考にして下さい。

人でもわかる型推論

型推論に関する最近の話題への雑感

可読性については各自で判断して下さい。

関数/クラスがテンプレートになっているのは少し使ってみたら分かるかと思います。後で触れます。

コンパイル速度と実行速度が速いというのは、コンパイルに関してはあんまり遅くなるような機能は入ってないですし、実行に関してもオーバーヘッドのあるようのことはしてないので必然速くなりそうです。LLVMですしCとGoの中間くらいの速度は出るんじゃないでしょうか。

メモリが安全というのはよく分かりませんでした。

文法

追記: 具体例による文法はこちらのGistも参照下さい Blawnの文法について

parser.yyを見ると文法が定義されています。

program = block block = lines lines = line | lines line line = line_content EOF | line_content END | definition | import import = "import" STRING_LITERAL EOL line_content = expression definition = function_definition | class_definition | c_type_definition | global_definition | c_function_declaration function_definition = "function" identifier arguments EOL block return_value EOL | "function" identifier arguments EOL return_value EOL class_definition = "class" identifier arguments EOL members_definition methods | "class" identifier arguments EOL members_definition | "class" identifier arguments EOL methods c_type_definition = "Ctype" identifier EOL c_members_definition methods = method_definition EOL | methods method_definition EOL method_definition = "@function" identifier arguments EOL block return_value ※ "@" identifierはサボって書いてますが "@" とidentifier繋げて1トークンです members_definition = "@" identifier "=" expression EOL | members_definition "@" identifier "=" expression EOL C_members_definition = "@" name "=" C_type_identifier EOL | C_members_definition "@" identifier "=" C_type_identifier EOL C_type_identifier = identifier | C_type_identifier IDENTIFIER C_arguments = C_type_identifier | C_arguments "," C_type_identifier C_returns = C_identifier return_value = "return" expression | "return" arguments = "( definition_arguments ")" definition_arguments = IDENTIFIER | definition_arguments "," IDENTIFIER global_definition = global EOL "(" EOL globals_variables EOL ")" EOL globals_variables = assign_variable | globals_variables EOL assign_variable c_function_declaration = "[Cfunction" identifier "]" EOL "arguments:" C_arguments EOL "return:" C_returns EOL | "[Cfunction" identifier "]" EOL "arguments:" EOL "return:" C_returns EOL expressions = expression | expressions "," expression expression = "if" expression EOL "(" EOL block ")" ※ if のないelseはプログラム側で弾いてます | "else" EOL "(" block ")" | "for" expression "," expression "," expression EOL "(" EOL block ")" | assign_variable | expression "<-" expression | expression "+" expression | expression "-" expression | expression "*" expression | expression "/" expression | expression "and" expression | expression "or" expression | expression ">=" expression | expression "<=" expression | expression ">" expression | expression "<" expression | expression "!=" expression | expression "==" expression | mononimal | list | access list = "{" expressions "} | "{" "}" ※ "." identifierはサボって書いてますが "." とidentifier繋げて1トークンです access = expression "." identifier assign_variable = identifier "=" expression monomial = call | STRING_LITERAL | FLOAT_LITERAL | INT_LITERAL | variable call = identifier "(" expressions ")" | identifier | access "(" expressions ")" | access "(" ")" variable = identifier identifier = [a-zA-Z_][0-9a-zA-Z_]* COMMENT = /\/\/.*

/ STRING_LITERAL = /"[^\"]*"/ INT_LITERAL = /[0-9]+/ FLOAT_LITERAL = /[0-9]+\.[0-9]*/

見ての通り行指向で、ちょくちょく改行が要求されます。

さて、例えば関数定義は以下のように書きます。

function hello(name) str = "Hello " str.append(name) print(str) return hello("blawn")

これを見てすぐさま「Python風の言語だ」「動的に型検査して遅くないの？」という声が上がってますが、ひとまず落ち着いて文法を読んで下さい。あとPython風の文法であるかと型を動的に検査するかは関係ないです。

文法を見ると、どこにもインデントを特別扱いしている箇所はありませんね。関数定義が return で終わっているだけです。

function_definition = "function" identifier arguments EOL block return_value EOL | "function" identifier arguments EOL return_value EOL

なので先の例は以下のようにインデントを潰して書いてもコンパイルが通ります。

function hello(name) str = "Hello " str.append(name) print(str) return hello("blawn")

関数は最後の1度しか return を書けない設計なので return を2度使おうとすると文法エラーになります。

if.blawn function if_return(b) if b ( return 1 ) else ( return 2 )

$ ./Blawn/blawn sample/if.blawn Error: syntax error at 4.5-10

逆に、 if は式なのでこのように return で if 式を返すことはできそうです。

function return_if(b) return if b ( 1 ) else ( 2 ) print(return_if(1 == 1))

が、実際にコンパイルするとセグフォってしまいました。

$ ./Blawn/blawn sample/if.blawn zsh: segmentation fault (core dumped) ./Blawn/blawn sample/if.blawn

ifを使うとCFGのノードが増えるので、そののハンドリングが必要なのを失念していたか、 if は本来は式ではなくlineだったかの誤りなんでしょう。

この他、ノードのハンドリングに由来すると思われるバグがいくつかあります。 確認してないですが、 for 式も同様のようです。

また、パーサで if else の関係を if を通ったかどうかのフラグで管理している関係上 else 節でさらに if else がくるとパースエラーになってしまいます。

if-nest.blawn if 1 == 1 ( print(1) ) else ( if 1 != 1 ( 2 ) else ( 3 ) )

$ ./Blawn/blawn sample/if-nest.blawn Error: else block without if block is valid.

これらはバグでしょうから、解決されるのを待ちましょう。

それはそれとして、今は if をネストさせたり for の中で if を使ったりしないという縛りの下プログラミングをしないといけません。

コード例

fizzbuzz

for の中に if を書いたり if の else 節にさらに else 節のある if を書いたりするとエラーになるので結構ハードモードです。幸い、関数を分ければ動くようなので以下のようなコードでfizzbuzzが書けます。

function divs(m, n) for 0, m <= n, 0 ( n = n - m ) return n == 0 function fizzbuzz_inner3(n) if divs(3, n) ( print("fizz") ) else ( print(int_to_str(n)) ) return function fizzbuzz_inner2(n) if divs(5, n) ( print("buzz") ) else ( fizzbuzz_inner3(n) ) return function fizzbuzz_inner(n) if divs(15, n) ( print("fizzbuzz") ) else ( fizzbuzz_inner2(n) ) return function fizzbuzz(n) for i = 0, i < n, i = i + 1 ( fizzbuzz_inner(i) ) return fizzbuzz(30)

for 式が恐らく初期化のあとに終了判定 と ループ終了後の処理 をしてからbodyに入っているようなので for 式のスタートは 0 ですが印字されるのは 1 はじまりです。

$ ./Blawn/blawn sample/fizzbuzz.blawn 1 2 fizz 4 buzz fizz 7 8 fizz buzz 11 fizz 13 14 fizzbuzz 16 17 fizz 19 buzz fizz 22 23 fizz buzz 26 fizz 28 29 fizzbuzz

型

静的に型検査されます。

例えば下記のように n に数値を代入したあとに文字列を代入すると

n = 1 n = "hoge"

コンパイルエラーになります。

Error: types are not same. i64 and %struct.String* at line 2

型検査時の型の保存先をLLVM IRにしてるせいで型名がLLVMのものになってますが、ちゃんとコンパイラが型を検査しています。

また、 構造的多相を採用したようで (see comment) 名前ではなく構造で整合性を検査するようで、クラスはメンバさえ合えば <- 演算子で代入できます。

sample/method.blawn class Person(name) @name = name @function getName() ret = self.name return ret class Animal(name) @name = name p1 = Person("κeen") p1 <- Animal("ポチ") print(p1.getName())

$ ./Blawn/blawn sample/method.blawn ポチ

さらに、これは構造的に部分になっていればいいので余計なフィールドを持つ Animal をアップキャストして Person 型の変数に代入できます。

class Animal(name, age) @name = name @age = age p1 = Person("κeen") p1 <- Animal("ポチ", 8)

ちなみにアップキャストがあると結構恐いことがあるんですがBlawnにはそれは起きませんでした

テンプレート

Blawnで引数をとるもの(関数、クラス)は全てテンプレートのようです。

例えば下記のように入力を数値として扱ったり文字列として扱ったり矛盾した関数を書いたとします。

function bad_function(input) input + 1 input.append("hoge") return

しかしこれだけではコンパイルエラーになりません。

呼び出したときにはじめてエラーになります。

例えば下記のように数値を与えると

bad_function(2)

append を呼んでいる箇所でエラーになります

Error: type i64 has no member append at line 2

一方文字列を与えると

bad_function("fuga")

(恐らく) + を呼んでいる箇所でエラーが起きます。

/path/to/Blawn/./data/llc: file.ll:83:15: error: invalid cast opcode for cast from '%struct.String*' to 'double' %2 = sitofp %struct.String* %1 to double ^

どちらのケースもテンプレートをインスタンス化したときにエラーが出ているのが見てとれると思います。

クラスも同様です。因みにクラスという名前ですが継承はサポートしていないようです。ちょっとリッチな構造体って感じですね。

下記のとおり T("hoge") や T(1) のように引数を渡すとその引数に合わせてインスタンス化されます。

class T(m) @m = m @function getM() ret = self.m return ret t1 = T("hoge") t2 = T(1) print(t1.getM()) print(int_to_str(t2.getM()))

ここで間違って t1 = t2 なんてすると、 T(string) に T(int) は代入できないのでちゃんと型エラーが返ってきます。

Error: types are not same. %T* and %T.0* at line 10

さて、Blawnはコンパイルが速いのが特徴と言われていました。コンパイラ自身 C++ で実装されていますし、あんまり遅くなるような機能がないので多分正しいでしょう。

しかしコンパイル時間がコードサイズの線形に伸びるかというとそうではなくて、以下のようないじわるなコードを考えると指数関数的にコンパイル時間が伸びます。

class T0(t0) @t0 = t0 class T1(t1) @t1 = t1 class T2(t2) @t2 = t2 class T3(t3) @t3 = t3 class T4(t4) @t4 = t4 class T5(t5) @t5 = t5 class T6(t6) @t6 = t6 class T7(t7) @t7 = t7 class T8(t8) @t8 = t8 class T9(t9) @t9 = t9 function f0(a0, a1, a2, a3, a4, a5) print("hello") return function f1(a1, a2, a3, a4, a5) f0(T0(1), a1, a2, a3, a4, a5) f0(T1(1), a1, a2, a3, a4, a5) f0(T2(1), a1, a2, a3, a4, a5) f0(T3(1), a1, a2, a3, a4, a5) f0(T4(1), a1, a2, a3, a4, a5) f0(T5(1), a1, a2, a3, a4, a5) f0(T6(1), a1, a2, a3, a4, a5) f0(T7(1), a1, a2, a3, a4, a5) f0(T8(1), a1, a2, a3, a4, a5) f0(T9(1), a1, a2, a3, a4, a5) return function f2(a2, a3, a4, a5) f1(T0(1), a2, a3, a4, a5) f1(T1(1), a2, a3, a4, a5) f1(T2(1), a2, a3, a4, a5) f1(T3(1), a2, a3, a4, a5) f1(T4(1), a2, a3, a4, a5) f1(T5(1), a2, a3, a4, a5) f1(T6(1), a2, a3, a4, a5) f1(T7(1), a2, a3, a4, a5) f1(T8(1), a2, a3, a4, a5) f1(T9(1), a2, a3, a4, a5) return function f3(a3, a4, a5) f2(T0(1), a3, a4, a5) f2(T1(1), a3, a4, a5) f2(T2(1), a3, a4, a5) f2(T3(1), a3, a4, a5) f2(T4(1), a3, a4, a5) f2(T5(1), a3, a4, a5) f2(T6(1), a3, a4, a5) f2(T7(1), a3, a4, a5) f2(T8(1), a3, a4, a5) f2(T9(1), a3, a4, a5) return function f4(a4, a5) f3(T0(1), a4, a5) f3(T1(1), a4, a5) f3(T2(1), a4, a5) f3(T3(1), a4, a5) f3(T4(1), a4, a5) f3(T5(1), a4, a5) f3(T6(1), a4, a5) f3(T7(1), a4, a5) f3(T8(1), a4, a5) f3(T9(1), a4, a5) return function f5(a5) f4(T0(1), a5) f4(T1(1), a5) f4(T2(1), a5) f4(T3(1), a5) f4(T4(1), a5) f4(T5(1), a5) f4(T6(1), a5) f4(T7(1), a5) f4(T8(1), a5) f4(T9(1), a5) return function f6() f5(T0(1)) f5(T1(1)) f5(T2(1)) f5(T3(1)) f5(T4(1)) f5(T5(1)) f5(T6(1)) f5(T7(1)) f5(T8(1)) f5(T9(1)) return f6()

コンパイル時間がコード量の1次関数でないという意味ではBlawnはコンパイルが遅い（遅くなる可能性を孕んでいる）言語に分類することができるかもしれません。

また、関数がテンプレートになっている関係上、関数を第一級の値として扱えなくなります。オブジェクトとメソッドを使えば関数と同等のことができるのでそこまで大きな問題じゃないんですが関数型プログラミングのファンは注意して下さい。

C FFI

サンプルにあるように .bridgeファイルにC FFIのバインディングを書けば以下のようにCの関数を呼べます。

下記はOpenGLの例ですね。

test.blawn import "gl.bridge" title = "this title is setted by Blawn!!".string window = create_window(title) draw(window)

gl.c #include <GL/glew.h> #include <GLFW/glfw3.h> #include <stdio.h> #include <string.h> #include <math.h> // ... GLFWwindow * create_window ( char * title ) { if ( glfwInit () == GL_FALSE ){ puts ( "error init

" ); return NULL ; } return glfwCreateWindow ( 640 , 480 , title , NULL , NULL ); } int draw ( GLFWwindow * window ) { if ( window == NULL ){ puts ( "Can't create GLFW window.

" ); return 1 ; } glfwWindowHint ( GLFW_CONTEXT_VERSION_MAJOR , 2 ); glfwWindowHint ( GLFW_CONTEXT_VERSION_MINOR , 1 ); glfwMakeContextCurrent ( window ); glfwSwapInterval ( 0 ); // ... return 0 ; }

glewやglfwをインストールの上、コンパイルは以下。

$ gcc -c gl.c -o gl.o $ ../../Blawn/blawn test1.blawn -l './gl.o -lGL -lglfw -lm -lGLEW'

アプリケーションはマウスを動かすと背景が変わるものみたいです。



ところでCのヘッダからC FFIバインディングを生成するツールが同梱されているようです。 コンパイラバイナリの下の tools/cridge.py がそれです。恐らく gl.bridge もそれを使って生成したものなのでしょう。よくできてますね。

C FFIがこのレベルで今動いているというのは相当すごいと思います。

まとめ

少しBlawnを触ってみたので紹介しました。

言語自作は知らない人には魔法のようで、少し齧った人には簡単で、真面目に取り組んだ人にはとてつもない偉業に見えるでしょう。

今回のはプログラマ以外にも広まったからか、過大評価したり過小評価したりする例が散見されました。

例えばもうCを捨ててBlawnを書いていくなんて言っている方も見かけましたが、流石にそれは早計でしょう。

逆に、このくらい半日でかけるおもちゃ言語だというのも少し外していると思います。

ゼロベースでflex, bison, LLVMを使い熟してC FFIのための周辺ツールまで整えているので一朝一夕ではできない芸当です。

言語の成熟に関しては作者本人のQuoraでの回答が一番落ち着いた考えだと思います。

プログラミング言語「Blawn」は普及しそうですか？に対するNaoto Ueharaさんの回答 - Quora

Blawnは今すぐ実用的な言語という訳ではないですから、気に入った人は成行を見守ればいいし興味のない人は忘れゆけばいいんじゃないでしょうか。