Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

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コードフォーマッタを比較してみた: 後編(翻訳)


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。