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
だけでできます。
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 = /\/\/.*\n/
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度使おうとすると文法エラーになります。
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 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) 名前ではなく構造で整合性を検査するようで、クラスはメンバさえ合えば <-
演算子で代入できます。
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にはそれは起きませんでした
くそぅ。悪いことできなかった。 pic.twitter.com/OsfovTAnn9
— κeen (@blackenedgold) October 22, 2019
# テンプレート一応解説しておくと、これはミュータブルかつ共変なコンテナ型にある不健全性を付くコードで、例えばJavaの配列だとこのように実行時例外を出せる。https://t.co/bXlZvEwfZE
— κeen (@blackenedgold) October 22, 2019
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の例ですね。
import "gl.bridge"
title = "this title is setted by Blawn!!".string
window = create_window(title)
draw(window)
#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\n");
return NULL;
}
return glfwCreateWindow(640,480,title,NULL,NULL);
}
int draw(GLFWwindow * window)
{
if (window == NULL){
puts("Can't create GLFW window.\n");
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は今すぐ実用的な言語という訳ではないですから、気に入った人は成行を見守ればいいし興味のない人は忘れゆけばいいんじゃないでしょうか。