903
341

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ウワサのBlawnを触ってみた

Last updated at Posted at 2019-10-23

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             = /\/\/.*\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度使おうとすると文法エラーになります。

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 は式なのでこのように returnif 式を返すことはできそうです。

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 を書いたり ifelse 節にさらに 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\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'

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

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

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

まとめ

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

言語自作は知らない人には魔法のようで、少し齧った人には簡単で、真面目に取り組んだ人にはとてつもない偉業に見えるでしょう。
今回のはプログラマ以外にも広まったからか、過大評価したり過小評価したりする例が散見されました。
例えばもうCを捨ててBlawnを書いていくなんて言っている方も見かけましたが、流石にそれは早計でしょう。
逆に、このくらい半日でかけるおもちゃ言語だというのも少し外していると思います。
ゼロベースでflex, bison, LLVMを使い熟してC FFIのための周辺ツールまで整えているので一朝一夕ではできない芸当です。

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

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

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

903
341
7

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
903
341

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?