Croud完成
クラウド・コンピューティング時代に適した、というよりもクラウドそのものを体現したプログラミング言語「Croud」を作ってみた。
- クラウドそのものを利用した可読性の高い文法
- 草原に寝そべって空を見上げているかのような、心和むソースリスト
- デフォルトでスタックを2本装備したチューリングコンプリートな設計
- 任意のタイミングでスタックを動的に拡張してスケールアウトが可能
Hello Worldは文字数が多くて面倒なので、以下は「Hello」を表示するコードの例。
$ cat hello.crd () () (_ ) () () (_ ) (_) (_) ( ) (_) (_) ( _) () () (_) () () () ( _) () () (__) (_) (_) (__) (_) (_) (_) ( _) (_) (_) ( ) () () () () ( ) () () () () (_) () (_) (_) (_) (_) ( _) (_) ( ) (_) (_) (__) (_) () () ( _) () () (_) () (_) () (_) (_) (__) (_) (_) (__) (_) (__) (_) () () ( _) () () (_) () () () (_) (_) ( _) (_) (_) (__) (_) ( ) ( ) () () (_ ) () () ( ) () () (_) () (_) (_) ( ) (_) (_) (_ ) (_) (_) (__) (_) $ ./croud hello.crd Hello$
そうでなくても腱鞘炎ぎみの手首が痛くなった。以下はフィボナッチ数列を計算する例。標準入力から受け取った数字の回数だけ数列を計算して停止する。
$ cat fib.crd () () (_ ) () () (_ ) (_) (_) ( ) (_) (_) ( _) () () (_) () () () ( _) () () (__) (_) (_) (__) (_) (_) (_) ( _) (_) (_) ( ) () () () () ( ) () () () () (_) () (_) (_) (_) (_) ( _) (_) ( ) (_) (_) (__) (_) () () ( _) () () (_) () (_) () (_) (_) (__) (_) (_) (__) (_) (__) (_) () () ( _) () () (_) () () () (_) (_) ( _) (_) (_) (__) (_) ( ) ( ) () () (_ ) () () ( ) () () (_) () (_) (_) ( ) (_) (_) (_ ) (_) (_) (__) (_) $ ./croud fib.crd 20 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946 17711 28657 $
使うモコモコは5種類。
() (_) 1 () ( ) 2 () (_) 3 (_) (__) 4 (_) (__) 5
用意されている命令セットとモコモコの対応は、
OPNUM = {11 => :push, 12 => :drop, 13 => :dup, 14 => :swap, 21 => :roll, 22 => :swit, 23 => :rot, 24 => :slide, 25 => :exp, 31 => :add, 32 => :sub, 33 => :mul, 34 => :div, 41 => :setl, 42 => :jmpl, 43 => :zjmpl, 44 => :nzjmpl, 51 => :puts, 52 => :gets, 53 => :putnum, 54 => :getnum}
という感じ。Whitespaceを真似して作ったけどヒープはない。代わりにスタックを2本にし、スタック操作の命令も多めにしてみた。制御関係は単純なジャンプとゼロ判定のジャンプが2つだけ。入出力は数値と文字が1つずつ扱える。
Croudは、去年暮れに出版されて一部で話題となった「Rubyで作る奇妙なプログラミング言語」にあったWhitespaceの実装を参考にして演習として作ってみたもので、いろいろと勉強になった。何事も自分でやるとよく分かる。CommonLispやJavaの入門書をあわせて読みつつ、だいたい次のようなことが分かった。
- Rubyのライブラリの一般的なファイル配置やインクルードの関係。
- 実は今まで謎だった、先頭からmodule宣言に囲まれたライブラリの意味。コロンコロン表記もちゃんと分かっていなかった。
- strscanライブラリの使い方。こうした機構は実はスキャナという一般名称で多くの言語で用意されているらしいことをJavaの入門書を読みつつ微かに理解した。確かにこれはよくある処理で、ずっと昔「しゃくとり虫法」という呼び名をつけてPerlで実践していたことがある。
- newメソッドではなくクラスメソッドを使った間接的なコンストラクタの呼び出し。newでオブジェクトを作って、それから何かの処理ためにオブジェクトにメッセージを送るという白々しい儀式を飛ばせる。
- 例外処理の重要性。しかし、遊びのプログラミングで、そんなものを几帳面に書くのは面倒すぎる。
- スタックを2本も用意すれば、実はチューリングマシンと等価になるということ。つまり、Croudは真にスケーラブ(ry
- 逆にスタック1本だけでは、スタック操作命令を少し増やしたぐらいではどう頑張ってもフィボナッチ数列すら処理の停止条件が書けないこと。
- スタックだヒープだテープだとかいうのはそれほど本質ではなくて、これらとまったく違うラムダ計算に基づいた処理系というものがあって、それは「計算とはベータ変換のこと」という謎めいた呪文に彩られているらしいこと。
- 謎めきつつもベータ変換は単純な記号処理っぽいので、自分で手を動かして計算すればYコンビネータの意味ぐらいはぼくにも分かるんじゃないかということ。
- つまりGrassはイカしてるってこと。
- この辺のことはオートマトンとかアブストラクトマシンがキーワードらしく、やっぱり体系立った勉強をしないと理解できないらしいこと。
- オートマトンを理解することとプログラミングスキルは直接関係しそうもないこと。
- 予約語を2行にまたがるようにすると、やたら処理が面倒なこと。単純に1行目と2行目をzipすれば、何とでもなると思ったけど、まあ面倒くさい。
- 予約語を2行にまたがるようにすると、やたらプログラミングが面倒なこと。しかし、慣れてくると1行目をずらずら書いてから改行して、その後に2行目を書けるようになるらしい。
- エソテリック系言語としては、命令セットを絞るか、トークンを絞るかしないとそれっぽくないということ。
ともあれ、以下、Croudのファイル構成とコード。
$ ls -lR .: 合計 0 drwxr-xr-x 2 yarb yarb 72 2009-02-09 11:00 bin drwxr-xr-x 2 yarb yarb 160 2009-02-10 00:20 example drwxr-xr-x 3 yarb yarb 96 2009-02-09 22:44 lib ./bin: 合計 4 -rwxr-xr-x 1 yarb yarb 127 2009-02-09 11:00 croud ./example: 合計 16 -rw-r--r-- 1 yarb yarb 329 2009-02-09 23:36 fib.crd -rw-r--r-- 1 yarb yarb 576 2009-02-10 00:20 hello.crd ./lib: 合計 4 drwxr-xr-x 2 yarb yarb 152 2009-02-09 23:14 croud -rw-r--r-- 1 yarb yarb 212 2009-02-09 22:44 croud.rb ./lib/croud: 合計 16 -rw-r--r-- 1 yarb yarb 1387 2009-02-09 10:57 compiler.rb -rw-r--r-- 1 yarb yarb 2386 2009-02-09 23:14 parse.rb -rw-r--r-- 1 yarb yarb 443 2009-02-09 10:57 stack.rb -rw-r--r-- 1 yarb yarb 1779 2009-02-09 11:05 vm.rb
(bin/croud) #!/usr/bin/ruby $LOAD_PATH.unshift File.expand_path("../lib", File.dirname(__FILE__)) require 'croud.rb' Croud.run(ARGF.read)
(lib/croud.rb) require 'croud/parse' require 'croud/compiler' require 'croud/vm' module Croud def self.run(src) asm = Croud::Parse.parse(src) inst = Croud::Compiler.compile(asm) Croud::VM.run(inst) end end
(lib/stack.rb) module Croud class Stack def initialize(n=2) @stc = [] n.times do @stc << [] end @cur = 0 end def stc @stc[@cur] end def switch @stc[@cur], @stc[@cur + 1] = @stc[@cur + 1], @stc[@cur] end def expand @stc << [] end def slide @stc[@cur + 1] << @stc[@cur].pop end def rotate @stc << @stc.shift end end end
(lib/parse.rb) require "strscan" SYMS = {" " => 0, "(" => 1, ")" => 2, "_" => 3} CLOUD_SHAPE = {"5b2" => "1", "582" => "2", "17a" => "3", "5fb2" => "4", "17fa" => "5"} OPNUM = {11 => :push, 12 => :drop, 13 => :dup, 14 => :swap, 21 => :roll, 22 => :swit, 23 => :rot, 24 => :slide, 25 => :exp, 31 => :add, 32 => :sub, 33 => :mul, 34 => :div, 41 => :setl, 42 => :jmpl, 43 => :zjmpl, 44 => :nzjmpl, 51 => :puts, 52 => :gets, 53 => :putnum, 54 => :getnum} module Croud class Parse def self.parse(src) new(src).parse end def initialize(src) @src = src.split(/\n/) @asm = "" end def parse nums = inst = "" while line = @src.shift next if line !~ /[\(\) _]/ nums += lpair2num(line, @src.shift) end s = StringScanner.new(nums) while cld = s.scan(/0*([15][^2a]+[2a])/) # each cloud if cld =~ /5(..)a/ # numeric literal inst += cld2num($1).to_s(16) else cld.delete!("0") inst += CLOUD_SHAPE[cld].to_s end end s = StringScanner.new(inst) while op = s.scan(/\d\d/) @asm += OPNUM[op.to_i].to_s if (op == "11") || (op == "41") || (op == "42") || (op == "43") || (op == "44") n = s.scan(/[0-9a-f]/) @asm += "," @asm += n end @asm += "\n" end @asm end def lpair2num(*lines) ul,dl = *lines nums = "" ul.split(//).zip(dl.split(//)).each do |pair| nums += pair2num(*pair) end nums end def pair2num(*pair) u,d = *pair if (u =~ /[ \(\)_]/) && (u =~ /[ \(\)_]/) num = SYMS[u] * 4 + SYMS[d] else raise "parse error" end num.to_s(16) end def cld2num(s) s.gsub("0","00").gsub("c","10").gsub("3","01").gsub("f","11").to_i(2) end end end if $0 == __FILE__ # puts Croud::Parse.parse(ARGF.read) src = <<EOS () () ( _) () () (__) () () (_) () (_) (_) (__) (_) (_) ( ) (_) (_) (__) (_) EOS puts Croud::Parse.parse(src) end =begin Some rules: ( ( = (( = 1 *4 + 1 = 5 ) _ = )_ = 2 * 4 + 3 = 11 = 0xb Therefore, () (_) 1 :5b2 () ( ) 2 :582 () (_) 3 :17a (_) (__) 4 :5fb2 (_) (__) 5 :17fa Numerics ( _) (_ ) :53ca -> 3c -> 01,10 -> 0x6 ( _) ( ) :50ca -> 0c > 0,10 -> 0x3 =end
(lib/compiler.rb) module Croud class Compiler def self.compile(asm) new(asm).compile end def initialize(asm) @asm = asm @inst = [] end def compile @asm.split(/\n/).each do |line| op,num = line.split(/,/) case op when "push" then @inst << :push << num.to_i(16) when "drop" then @inst << :drop when "dup" then @inst << :dup when "swap" then @inst << :swap when "roll" then @inst << :roll when "swit" then @inst << :swit when "rot" then @inst << :rot when "slide" then @inst << :slide when "exp" then @inst << :exp when "add" then @inst << :add when "sub" then @inst << :sub when "mul" then @inst << :mul when "div" then @inst << :div when "setl" then @inst << :setl << num.to_i(16) when "jmpl" then @inst << :jmpl << num.to_i(16) when "zjmpl" then @inst << :zjmpl << num.to_i(16) when "nzjmpl" then @inst << :nzjmpl << num.to_i(16) when "puts" then @inst << :puts when "putnum" then @inst << :putnum when "gets" then @inst << :gets when "getnum" then @inst << :getnum end end @inst end end end if $0 == __FILE__ asm = <<EOS push,a puts setl,1 rot exp swit EOS p Croud::Compiler.compile(asm) end
(lib/vm.rb) require 'croud/stack' module Croud class VM def self.run(inst) new(inst).run end def initialize(inst) @inst = inst @label = {} end def mk_jump_table @inst.each_with_index do |op, i| @label[@inst[i + 1]] = i if op == :setl end end def run self.mk_jump_table pc = 0 s = Croud::Stack.new while pc < @inst.size op = @inst[pc] case op when :push pc += 1 num = @inst[pc] s.stc << num when :drop s.stc.pop when :dup s.stc << s.stc.last when :swap x, y = s.stc.pop, s.stc.pop s.stc << x << y when :roll s.stc << s.stc.delete_at(-3) when :swit s.switch when :rot s.rotate when :slide s.slide when :exp s.expand when :add s.stc << s.stc.pop + s.stc.pop when :sub y, x = s.stc.pop, s.stc.pop s.stc << x - y when :mul s.stc << s.stc.pop * s.stc.pop when :div y, x = s.stc.pop, s.stc.pop s.stc << x / y when :jmpl pc = @label[@inst[pc + 1]] when :zjmpl pc = @label[@inst[pc + 1]] if s.stc.last == 0 when :nzjmpl pc = @label[@inst[pc + 1]] if s.stc.last != 0 when :puts print s.stc.last.chr when :gets s.stc << $stdin.gets.chomp! when :putnum print s.stc.last puts when :getnum s.stc << $stdin.gets.chomp!.to_i end pc += 1 end end end end if $0 == __FILE__ Croud::VM.run([:push,72,:push,73,:swap,:puts,:drop,:puts]) puts end