こんにちは、hachi8833です。今年もアドベントカレンダーの季節がやってまいりました。だいぶ間が空いてしまいましたが、Goby記事です。
- リポジトリ: goby-lang/goby
- 公式サイト: https://goby-lang.org/
Ripperクラス
2018年9月に、GobyにRipper
ライブラリを追加しました。Rubyのライブラリと同じ名前で恐縮です。
RubyのRipperはクラスメソッドとインスタンスメソッドを両方備えていますが、Gobyのは今のところクラスメソッドのみです。require 'ripper'
して使う点はGobyも同じです。
RubyのRipper
RubyのRipperは、Rubyの標準ライブラリに含まれているパーサーです。Rubyの構文解析を追ったり静的解析を行ったりするときに使われます。なおRubocopではある時期からRipperを使わなくなったそうです(関連記事)。
# 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
クラスなので、require
をrequire :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は、String
やInteger
などと同じネイティブクラスとして作り始めたのですが、常駐させることもないと思い、ローダブルなライブラリに変更して必要なものを以下のように読み込んでいます。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の中身
tokenize
、parse
、lex
、instruction
メソッドは作りとして大差はないので、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をそこそこ大掛かりに変えないといけなさそうで悩んでます。まあ趣味なのでちびちび進めるつもりです。