Hatena::ブログ(Diary)

Konbuの技術 && 読書録 このページをアンテナに追加 RSSフィード Twitter

人気ブログランキングへ にほんブログ村 本ブログへ にほんブログ村 IT 技術ブログ Linuxへ 人気ブログランキング ブログ王へ

2012-04-29

Create Your Own Programming Language

以前から言語を作る事に興味がわいていたのですが、中々難しそうに見えて尻込みして挑戦出来ていませんでした。それが、昨日急に言語を作りたい衝動にかられて wikipedia 巡りをしてしまいました。最近盛り上がってきつつあり、Rails にも採用された CoffeeScript は自分の中でも感動した言語だったので当然 wikipedia を参照したわけです。

CoffeeScript - wikipedia

CoffeeScript電子書籍である "Create Your Own Language" を読んで開発された。

上記引用の通り、電子書籍の Create Your Own Programming Language を読んで開発されたらしく、勢い勇んで洋書に手を出しました。もう勢いです。

以下のリンクから購入出来ます。

Create Your Own Programming Language

ページを開いていきなり "あなたが考えているより簡単に初めての言語を作れる" なんて謳い文句までついてくる始末。

英語も幸いにしてあまり難しくない表現で書かれているので僕程度の英語力でもなんとかなっています。問題は単語ですが、そこは iPad 様々ですかね。

全部で 160 ページくらいの薄い物なんで抵抗もあまり無く、(とは言っても未だ 30 ページ程度ですが。)読めているので

この本はすぐに最後まで読み切れそうです。

現在は Lexer を Ruby で書く部分です。今回の記事ではそのソースコードの解読をしておこうと思います。

lexer.rb

class Lexer
  # KEYWORDS では 言語内で用いる予約語を定義しているようです
  # TOY 言語に必要な機能のみを実装する事を目的にしているのであまり予約語は無いです
  KEYWORDS = ["def", "class", "if", "true", "false", "nil"]

  # tokenize ではトークンの生成を行なっています
  # code はプログラムのコードを受け取ります
  def tokenize(code)
    # 改行コード等 code の最後の特殊なコードの削除
    code.chomp!

    i = 0
    tokens = []
    current_indent = 0
    indent_stack = []

    while i < code.size
      # i の挙動を見る為に僕が仕込んだコードです
      # 本来必要の無いコード
      p i

      # 残っている code の抜き出し
      chunk = code[i..-1]

      # トークンの生成
      # String class の slice で regexp に最初にマッチする文字の抜き出し
      if identifier = chunk[/\A([a-z]\w*)/, 1]
        if KEYWORDS.include?(identifier)
          tokens << [identifier.upcase.to_sym, identifier]
        else
          tokens << [:IDENTIFIER, identifier]
        end
        # identifier のサイズ分だけ文字探査を進める
        i += identifier.size

      elsif constant = chunk[/\A([A-Z]\w*)/, 1]
        tokens << [:CONSTANT, constant]
        i += constant.size
        
      elsif number = chunk[/\A([0-9]+)/, 1]
        tokens << [:NUMBER, number.to_i]
        i += number.size

      elsif string = chunk[/\A"(.*?)"/, 1]
        tokens << [:STRING, string]
        i += string.size + 2

      # : 以降の indent サイズのチェック(if, def等)
      elsif indent = chunk[/\A\:\n( +)/m, 1]
        if indent.size <= current_indent
          raise "Bad indent level, got #{indent.size} indents." +
            "expected > #{current_indent}"
        end
        current_indent = indent.size
        indent_stack.push(current_indent)
        tokens << [:INDENT, indent.size]
        i += indent.size + 2

      # indent サイズのチェック
      elsif indent = chunk[/\A\n( *)/m, 1]
        if indent.size == current_indent
          tokens << [:NEWLINE, "\n"]
        elsif indent.size < current_indent
          while indent.size < current_indent
            indent_stack.pop
            current_indent = indent_stack.first || 0
            tokens << [:DEDENT, indent.size]
          end
          tokens << [:NEWLINE, "\n"]
        else
          raise "Missing"
        end
        i += indent.size + 1
      elsif operator = chunk[/\A(\|\||&&|==|!=|<=|>=)/, 1]
        tokens << [operator, operator]
        i += operator.size
      elsif chunk.match(/\A /)
        i += 1
      else
        value = chunk[0, 1]
        tokens << [value, value]
        i += 1
      end
    end

    while indent = indent_stack.pop
      tokens << [:DEDENT, indent_stack.first || 0]
    end

    tokens
  end
end

コメントアウトで説明を入れては見ましたが、以外とコードを見れば分かる内容 && メンド(ry なので後半はあまりコメント入れてません。

lexer_test.rb

require "test/unit"
require "lexer"

class LexerTest < Test::Unit::TestCase
  def setup
    @code = <<-CODE
if 1:
  print "..."
  if false:
    pass
  print "done!"
print "The End"
    CODE

    @tokens = [
               [:IF, "if"], [:NUMBER, 1],
               [:INDENT, 2], [:IDENTIFIER, "print"], [:STRING, "..."],
               [:NEWLINE, "\n"], [:IF, "if"], [:FALSE, "false"], [:INDENT, 4],
               [:IDENTIFIER, "pass"],
               [:DEDENT, 2], [:NEWLINE, "\n"], [:IDENTIFIER, "print"],
               [:STRING, "done!"],
               [:DEDENT, 0], [:NEWLINE, "\n"],
               [:IDENTIFIER, "print"], [:STRING, "The End"]
              ]
  end
  
  def test_tokenize
    assert_equal @tokens, Lexer.new.tokenize(@code)
  end
end

上記テストコードを以下の様に実行すると、

>|

$ ruby test/lexer_test.rb

Loaded suite test/lexer_test
Started
0
2
3
4
8
13
14
19
22
24
25
30
36
40
43
48
49
56
57
62
63
.
Finished in 0.003022 seconds.

1 tests, 1 assertions, 0 failures, 0 errors
こんな感じでテストが完了しました。 lexer_test.rb の中にある @tokens に本来出力されるはずの中身を書いているので、Lexer.new.tokenize(@code) の中身を目視でも確認しておくと理解が早まるのかなとも思いました。 tokenize の一行目で p i している為 i の挙動が表示されて何となくコードの流れがつかめたのは個人的に良かったかなと思ったり... 構文の解析はまだまだ先は長そうですが、この程度の長さのコードなら原理を学ぶのは出来そうです。 今回の Lexer の話はこれで終わりです。 それではまた本の中に戻ります。

2012-03-03

Rails っぽいコンフィグ

前回 MySQLRuby から操作出来るようにしたので、今回は Rails のように開発版と本番の切り替えを簡単に行えるようにしました。

とは言え、サーバの動作時に切り替えるようなものではなく、スクリプトが動作するときに指定した設定で読むだけのものです。

db.yml

:usetype: :developement

:developement:
  :database: db
  :host: host
  :user: user
  :pass: pass

:production:
  :database: db
  :host: host
  :user: user
  :pass: pass

上記の設定ファイルを読みこんで、MySQL の設定を抜き出すようにしています。

require 'mysql'
class SQL
  def initialize
    @list = YAML.load(File.read("db.yml"))
  end

  def type
    @list[@list[:usetype]]
  end
end

sql = SQL.new.type

これで、

db = Mysql::new(sql[:host], sql[:user], sql[:pass], sql[:database])

とすれば、db.yml の :usetype で指定した設定が引き出せます。

結構簡単に切り替えが出来るんですね。データベースの操作についてはまだ触り始めで勉強中なので今回はここまでにしておきます。

2012-03-02

gem で mysql のインストールに必要な物

$ gem install mysql -- --with-mysql-config

しようとしたら失敗したのでそのログ。

結果から言うと、どうも libmysqlclient-dev が無くてつまずいていたっぽい。mysql ってクライアントサーバ入れて更に別個の dev パッケージを入れる必要があって面倒くさいですね。

libmysqlclient-dev 入れたら普通に通りました。

2012-02-29

Ruby on Rails 3 ポケットリファレンス

かなり遅くなりましたが、Wings さんより献本頂いた本を読み終えました。

ポケットリファレンスRails 3 版です。

Ruby on Rails 3 ポケットリファレンス

Ruby on Rails 3 ポケットリファレンス

Rails 3 の導入から始まり、各メソッドについて詳しく書かれています。ここからの入門としては開発の流れがこの本では分からないですが、Web にある情報とこの本だけでも十分に動くものを作れる内容です。

また、リファレンスとしての内容も非常に細かく解説を行われているため、そうそう困ることはなさそうです。

特に個人的にはテストの項目が詳しく、為になりました。なにせ、 Rails でのテストの作法とか良くわかりませんから。標準のライブラリである Unit だったのも個人的には○でした。いきなり他のを使うこと前提とか良くわかんないですし。それに古めの内容にはなるものの RSpec を使う場合の参考も載せているのである程度どちらも補完しているといえます。

付録にて解説されている CoffeeScript や SCSS についても基本的な説明を行われているので、これを機に触ってみようと言う人には十分な内容でした。

僕が CoffeeScript を勉強し始めたときは日本語の解説がなく不便でしたが、この辺の日本語化は最近早いですね。

2012-02-08

StumpWM 上でもうひとつ WM 動作

数カ月ぶりのブログ更新です。

ほんとは大学院の公聴会が終わったら直ぐに更新しようと思っていたんですが、面倒臭がってグダグダしてしまいました。

今回は StumpWM 上で GUI アプリをつくろうと思った時に Window が目一杯のサイズで出てくるのが鬱陶しかったため、開発用に WM を動かすことにしました。以前から目をつけていたんですが、やはりメンドクサクて放置。そして必要にかられて今回挑戦したわけです。

殆ど タイル型 window manager だけどウィンドウを float 制御したい という記事を使いました。異なる点といえば openbox を使用するように変更した事と、run-or-raise の恩恵をあずかるようにしたくらいです。

Xephyr, openbox 導入

$ sudo aptitude install xserver-xephyr openbox

floatwm.sh

#!/bin/sh

~/stumpwm/contrib/stumpish gnew openbox
Xephyr -ac -br -fullscreen -host-cursor -class xephyr :1 &
sleep 1
DISPLAY=:1 openbox &

kill_openbox.sh

#!/bin/sh

~/stumpwm/contrib/stumpish gselect openbox
~/stumpwm/contrib/stumpish gkill
killall Xephyr

.stumpwmrc

(defcommand float-openbox () ()
            (run-or-raise "/path/to/bin/floatwm.sh" '(:class "Xephyr")))

(defcommand kill-float-openbox() ()
            (run-or-raise "/path/to/bin/kill_openbox.sh" '(:class "kill-openbox")))

ほんとに修正したところは少ないですが、コレでグループ切り替えしつつ openbox を起動できます。勿論 run-or-raise してるので 2 回目以降の float-window コマンドでは既に開いている openbox Group にフォーカスを移します。また、kill-float-window で openbox Group を Xephyr 毎切れます。

これで GUI アプリを作る環境を X201s に用意出来ました。次回は GUI で記事が書けるといいなぁ。