Goby: Rubyライクな言語(7)Ripperライブラリを追加した

こんにちは、hachi8833です。今年もアドベントカレンダーの季節がやってまいりました。だいぶ間が空いてしまいましたが、Goby記事です。

Goby: Rubyライクな言語(2)Goby言語の全貌を一発で理解できる解説スライドを公開しました!

Ripperクラス

2018年9月に、GobyにRipperライブラリを追加しました。Rubyのライブラリと同じ名前で恐縮です。

RubyのRipperはクラスメソッドとインスタンスメソッドを両方備えていますが、Gobyのは今のところクラスメソッドのみです。require 'ripper'して使う点はGobyも同じです。

RubyのRipper

RubyのRipperは、Rubyの標準ライブラリに含まれているパーサーです。Rubyの構文解析を追ったり静的解析を行ったりするときに使われます。なおRubocopではある時期からRipperを使わなくなったそうです(関連記事)。

参考: class Ripper (Ruby 2.6.0)

# Ruby 2.6.5
require 'ripper'
Ripper.lex "[1, 2, 3].map do |i| i * i end"
# 出力(改行を追加しています)
[
  [[1, 0], :on_lbracket, "[", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>], 
  [[1, 1], :on_int, "1", #<Ripper::Lexer::State: EXPR_END>], 
  [[1, 2], :on_comma, ",", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 3], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 4], :on_int, "2", #<Ripper::Lexer::State: EXPR_END>],
  [[1, 5], :on_comma, ",", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 6], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 7], :on_int, "3", #<Ripper::Lexer::State: EXPR_END>],
  [[1, 8], :on_rbracket, "]", #<Ripper::Lexer::State: EXPR_END>],
  [[1, 9], :on_period, ".", #<Ripper::Lexer::State: EXPR_DOT>],
  [[1, 10], :on_ident, "map", #<Ripper::Lexer::State: EXPR_ARG>],
  [[1, 13], :on_sp, " ", #<Ripper::Lexer::State: EXPR_ARG>],
  [[1, 14], :on_kw, "do", #<Ripper::Lexer::State: EXPR_BEG>],
  [[1, 16], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG>],
  [[1, 17], :on_op, "|", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 18], :on_ident, "i", #<Ripper::Lexer::State: EXPR_ARG>],
  [[1, 19], :on_op, "|", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 20], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG|EXPR_LABEL>],
  [[1, 21], :on_ident, "i", #<Ripper::Lexer::State: EXPR_END|EXPR_LABEL>],
  [[1, 22], :on_sp, " ", #<Ripper::Lexer::State: EXPR_END|EXPR_LABEL>],
  [[1, 23], :on_op, "*", #<Ripper::Lexer::State: EXPR_BEG>],
  [[1, 24], :on_sp, " ", #<Ripper::Lexer::State: EXPR_BEG>],
  [[1, 25], :on_ident, "i", #<Ripper::Lexer::State: EXPR_END|EXPR_LABEL>],
  [[1, 26], :on_sp, " ", #<Ripper::Lexer::State: EXPR_END|EXPR_LABEL>],
  [[1, 27], :on_kw, "end", #<Ripper::Lexer::State: EXPR_END>]
]

GobyのRipper

GobyのRipperは、今のところ以下のクラスメソッドのみです。

  • tokenize
  • parse
  • lex
  • instruction

なおGobyではシンボルが常にStringクラスなので、requirerequire :ripperのようにシンボル形式でも書けます😋。

# Goby 0.1.11
require :ripper
Ripper.lex("[1, 2, 3].map do |i| i * i end")
# 出力(改行を追加しています)
[
  [0, "on_lbracket", "["], 
  [0, "on_int", "1"], 
  [0, "on_comma", ","], 
  [0, "on_int", "2"], 
  [0, "on_comma", ","], 
  [0, "on_int", "3"], 
  [0, "on_rbracket", "]"], 
  [0, "on_dot", "."], 
  [0, "on_ident", "map"], 
  [0, "on_do", "do"], 
  [0, "on_bar", "|"], 
  [0, "on_ident", "i"], 
  [0, "on_bar", "|"], 
  [0, "on_ident", "i"], 
  [0, "on_asterisk", "*"], 
  [0, "on_ident", "i"], 
  [0, "on_end", "end"], 
  [0, "on_eof", ""]
 ]

今のところは、だいぶあっさりした出力です。

importは以下のようになっています。

package ripper

import (
    "fmt"
    "github.com/goby-lang/goby/compiler"
    "github.com/goby-lang/goby/compiler/bytecode"
    "github.com/goby-lang/goby/compiler/lexer"
    "github.com/goby-lang/goby/compiler/parser"
    "github.com/goby-lang/goby/compiler/token"
    "github.com/goby-lang/goby/vm"
    "github.com/goby-lang/goby/vm/classes"
    "github.com/goby-lang/goby/vm/errors"
    "strings"
)

一見してわかるように、Gobyに備わっているコンパイラを直接importして使っています。基本的にGobyのRipperは、コンパイラのparserやlexerやtokenなどの結果を正直に吐き出しているだけです。だからこそ私でもどうにか作れました。

当初のRipperは、StringIntegerなどと同じネイティブクラスとして作り始めたのですが、常駐させることもないと思い、ローダブルなライブラリに変更して必要なものを以下のように読み込んでいます。require :ripperするまでアクティベートされません。

// Imported objects from vm
type Object = vm.Object
type VM = vm.VM
type Thread = vm.Thread
type Method = vm.Method
type StringObject = vm.StringObject
type HashObject = vm.HashObject
type ArrayObject = vm.ArrayObject

GobyのローダブルライブラリのGoコードは、以下のようにinitでクラスメソッドとインスタンスメソッドを登録しておきます。

// Internal functions ===================================================
func init() {
    vm.RegisterExternalClass("ripper", vm.ExternalClass("Ripper", "ripper.gb",
        // class methods
        map[string]vm.Method{
            "instruction": instruction,
            "lex":         lex,
            "new":         new,
            "parse":       parse,
            "tokenize":    tokenize,
        },
        // instance methods
        map[string]vm.Method{},
    ))
}

ただしGobyのローダブルライブラリでは、上の他にダミーのRipperクラスをripper.gbというファイルとして用意しておく必要があります。requireすると、上のinitでripper.gbとripper.goがバインドされます。

# A dummy class for ripper.go
class Ripper
end

Ripperの中身

tokenizeparselexinstructionメソッドは作りとして大差はないので、tokenizeメソッドを例に取ります。

func tokenize(receiver Object, sourceLine int, t *Thread, args []Object) Object {
    if len(args) != 1 {
        return t.VM().InitErrorObject(errors.ArgumentError, sourceLine, "Expect 1 argument. got=%d", len(args))
    }

    arg, ok := args[0].(*StringObject)
    if !ok {
        return t.VM().InitErrorObject(errors.TypeError, sourceLine, errors.WrongArgumentTypeFormat, classes.StringClass, args[0].Class().Name)
    }

    l := lexer.New(arg.Value().(string))
    el := []Object{}
    var nt token.Token
    for i := 0; ; i++ {
        nt = l.NextToken()
        if nt.Type == token.EOF {
            el = append(el, t.VM().InitStringObject("EOF"))
            break
        }
        el = append(el, t.VM().InitStringObject(nt.Literal))
    }
    return t.VM().InitArrayObject(el)
}

lexer.Newのようにコンパイラ配下のパッケージで.Newし、文字列を食わせた結果をarrayとして返しているだけです。

instructionは、階層が2つに固定されているのを利用してArrayやHashに変換しています。

func convertToTuple(instSet []*bytecode.InstructionSet, v *VM) *ArrayObject {
    ary := []Object{}
    for _, instruction := range instSet {
        hashInstLevel1 := make(map[string]Object)
        hashInstLevel1["name"] = v.InitStringObject(instruction.Name())
        hashInstLevel1["type"] = v.InitStringObject(instruction.Type())
        if instruction.ArgTypes() != nil {
            hashInstLevel1["arg_types"] = getArgNameType(instruction.ArgTypes(), v)
        }
        ary = append(ary, v.InitHashObject(hashInstLevel1))

        arrayInst := []Object{}
        for _, ins := range instruction.Instructions {
            hashInstLevel2 := make(map[string]Object)
            hashInstLevel2["action"] = v.InitStringObject(ins.ActionName())
            hashInstLevel2["line"] = v.InitIntegerObject(ins.Line())
            hashInstLevel2["source_line"] = v.InitIntegerObject(ins.SourceLine())

            arrayParams := []Object{}
            for _, param := range ins.Params {
                arrayParams = append(arrayParams, v.InitStringObject(covertTypesToString(param)))
            }
            hashInstLevel2["params"] = v.InitArrayObject(arrayParams)

            if ins.Opcode == bytecode.Send {
                hashInstLevel1["arg_set"] = getArgNameType(ins.Params[3].(*bytecode.ArgSet), v)
            }

            arrayInst = append(arrayInst, v.InitHashObject(hashInstLevel2))
        }

        hashInstLevel1["instructions"] = v.InitArrayObject(arrayInst)
        ary = append(ary, v.InitHashObject(hashInstLevel1))
    }
    return v.InitArrayObject(ary)
}

Ripperのバグ修正

実はマージ後しばらくしてRipper.instructionの動作がおかしくなっていました。この間それを思い出してやっと直しました😅(#804)。

最近のGoby

今、GobyにもRubyのような文字列の式展開機能を付けたいと思ってゴニョゴニョ調べています。現在のGobyの構文解析はシンプルなのですが、式展開をやるにはlexerやparserやASTをそこそこ大掛かりに変えないといけなさそうで悩んでます。まあ趣味なのでちびちび進めるつもりです😊。

関連記事

Rubyの式展開(string interpolation)についてまとめ: `#{}`、`%`、Railsの`?`

RuboCop作者がRubyコードフォーマッタを比較してみた: 後編(翻訳)

デザインも頼めるシステム開発会社をお探しならBPS株式会社までどうぞ 開発エンジニア積極採用中です! Ruby on Rails の開発なら実績豊富なBPS

この記事の著者

hachi8833

Twitter: @hachi8833、GitHub: @hachi8833 コボラー、ITコンサル、ローカライズ業界、Rails開発を経てTechRachoの編集・記事作成を担当。 これまでにRuby on Rails チュートリアル第2版の監修および半分程度を翻訳、Railsガイドの初期翻訳ではほぼすべてを翻訳。その後も折に触れて更新翻訳中。 かと思うと、正規表現の粋を尽くした日本語エラーチェックサービス enno.jpを運営。 実は最近Go言語が好きで、Goで書かれたRubyライクなGoby言語のメンテナーでもある。 仕事に関係ないすっとこブログ「あけてくれ」は2000年頃から多少の中断をはさんで継続、現在はnote.muに移転。

hachi8833の書いた記事

BPSアドベントカレンダー

週刊Railsウォッチ