godef

久しぶりに、無線のネタ。友人に教えてもらった。

HAM STUDY

これ、アメリカ版のアマチュア無線ライセンス取得用の予備校って言うか、対策サイト。

御多聞に漏れず、試験問題が多数プールされてて、そこからチョイスして本試験の問題が出るとの事。だったら、そのプールにある問題を制覇しておけば、合格間違い無しって寸法。

このサイトはその問題がプールされてるとの事。これで、自分の弱点を知る訳ですな。傾向と対策で、合理的。

但し、プールされる問題は、時代に即して3-4年毎に改定されるので、常に最新動向を追っておく必要があるらしい。昔勉強したから大丈夫ってのは通用しない。日本のそれとは大違いだそうだ。

ライセンスは、ノビス級(初心者)から始まって、テクニシャン、ジェネラル、エキストラと順番に取得する事が必要らしい。日本みたいに、腕に覚えがあるんで、いきなり1級を受験ってのは出来ないとの事。ちゃんと愛好家を育てるように制度設計がされてる訳ね。

この試験は、日本でも受験可能との事。アメリカの無線連盟が認定した試験官が日本にも居て、試験が行われるそうだ。

そして試験は、易しい級から順番に行われ、その場で合否が出るので、合格したら次のライセンスの試験を受けられるとの事。頑張れば、一日で、ライセンスを幾つも保持出来るらしい。

で、それに挑戦するんだと言って、友人はエンジンをかけたみたい。頑張って、エキストラ級を1日で取得してください。

ああ、アメリカの試験を日本でやるんで、日本語化されてる? そんな事は無い。原語での試験ですから、まずは英語がスラスラ理解出来ないと駄目だわな。

There are three license classes in ham radio. In order they are: Technician
(Element 2), General (Element 3), and Amateur Extra (Element 4). Each has an
associated written examination that you must pass to earn your amateur radio
license.

これを見ると、ノビス級ってのはもう無いのかな。まて、Element 1 が、出てきていないだけで、未だに健在なの? CWを叩けるって言う技能試験は無くて、筆記試験だけか。酷試はどうなってるのだろう? CWは、世界遺産だから、もう無視か。つまらん世の中だのう。

TAGS

前回の最後にやったetagsを使って、golangのsrcが提供してるファイル群のTAGSを作成してみた。

root@usvr:/usr/local/go/src# etags `find . -name '*.go' | grep -v _test.go`

ルートエリアにTAGSが出来上がるので、root権限で。

sakae@usvr:/usr/local/go/src$ egrep '^package' TAGS  | wc
   2915    5865   58200
sakae@usvr:/usr/local/go/src$ egrep '^type' TAGS  | wc
   4542   13625  112894
sakae@usvr:/usr/local/go/src$ egrep '^func' TAGS  | wc
  38452  100048 1237085
sakae@usvr:/usr/local/go/src$ fgrep 'func (' TAGS  | wc
  11739   46643  445565

そして、ちょいと統計を取ってみた。パッケージの数がファイルの個数になるのかな。新しい型の宣言って沢山あるな。関数は一ファイルに10個以上定義されてる割合だ。そのうちのインターフェースを満たすやつが1/4強あるとな。

sakae@usvr:~/go/src$ etags `find . -name '*.go' | grep -v _test.go`

自分のエリアでもやってみた。これを使えば、前回の不具合(webdav.Handlerへ飛んでいかない)が、解決されたよ。ただ、惜しい事に同名定義が沢山出て来て、選ぶのに苦労するきらいはあるけどね。 もう少し、パッケージングの方式を考慮してくれる事を望む。(まあ、無理な相談だけどもね)

go-mode.el

go-modeから使うgodefで上手くjump出来なかった問題、何処に問題が有るか探ってみる事にする。取っ掛かりは、emacs用のパッケージからだな。

cent:~$ cd .emacs.d/elpa/go-mode-20181012.329/
cent:go-mode-20181012.329$ e

うろ覚えの名前、godef-jumpを頼りに探すと、

    (define-key m (kbd "C-c C-j") #'godef-jump)

こんなキーバインドが見つかった。これ、前回はxrefのキーバインドに合わせるために、別の物に置き換え、その過程で古いemacsを使ってる事が発覚。遂にはemacsを最新にするという暴挙に出たやつだ。デフォで、キーバインドされてるじゃん、と、プチ恨み節。

兎も角、gode-jump経由で、godefを呼び出す所を突き止めた。

(defun godef--call (point)
  "Call godef, acquiring definition position and expression
description at POINT."
  (if (not (buffer-file-name (go--coverage-origin-buffer)))
      (error "Cannot use godef on a buffer without a file name")
    (let ((outbuf (generate-new-buffer "*godef*"))
          (coding-system-for-read 'utf-8)
          (coding-system-for-write 'utf-8))
      (prog2
          (call-process-region (point-min)
                               (point-max)
                               godef-command
                               nil
                               outbuf
                               nil
                               "-i"
                               "-t"
                               "-f"
                               (file-truename (buffer-file-name (go--coverage-origin-buffer)))
                               "-o"
                               ;; Emacs point and byte positions are 1-indexed.
                               (number-to-string (1- (position-bytes point))))
          (with-current-buffer outbuf
            (split-string (buffer-substring-no-properties (point-min) (point-max)) "\n"))
        (kill-buffer outbuf)))))

この関数には、カーソルポジションが渡って来るんだな。ポイントが属するエリアを引数にして、godefコマンドを呼び出し。後はその結果を解析か。

この関数を深堀してもいいんだけど、際限がなくなりそうなので、気が付いた所だけ。call-process-refionの引数にnilが混じってるけど何だろう? -fで渡されるのはgodefが理解出来るファイル名なんだろうね。実際の値がどんなになるか、確認したい所。

そういう場合は、elisp用のdebiggerを登場させる? えーとどうやるんだ、ってんで軽く調べてみたら、昔ながらのprintでバックが、いの一番に紹介されてた。

(message "%s" '(1 2 3 4))
"(1 2 3 4)"

*scratch* で、上記のS式を入力してC-j するとevalした結果が表示された。C-x C-e だと、結果はmini-bufferに出て来るな。どちらでもお好きな方を使えばよい。

format指示子に何の考えもなく%sを指定しちゃったけど、他にはどんなのが有る? formatを引くと

%s means print a string argument.  Actually, prints any object, with `princ'.
%d means print as signed number in decimal.
%o means print as unsigned number in octal, %x as unsigned number in hex.
%X is like %x, but uses upper case.
%e means print a number in exponential notation.
%f means print a number in decimal-point notation.
%g means print a number in exponential notation if the exponent would be
   less than -4 or greater than or equal to the precision (default: 6);
   otherwise it prints in decimal-point notation.
%c means print a number as a single character.
%S means print any object as an s-expression (using `prin1').

まあ、一般常識が通用すると思ってればいいか。schemeみたいに変な前置詞じゃないだけ素直。

あれこれ考えた挙句、godefへどんな引数が渡っているかは、godef側で調べた方が良かろう。 そりゃそうだ。送り出し側で幾ら調べたって、受け取り側で確認しないと、途中でロストしてる可能性が有るからね。これ常識。

godoc

その前に、godefの引数解説を探さねば。

cent:~$ godoc godef
2018/11/16 14:05:33 error while importing build package: cannot find package "godef" in any of:
        /usr/local/go/src/godef (from $GOROOT)
        /home/sakae/go/src/godef (from $GOPATH)
2018/11/16 14:05:33 cannot find package "." in:
        /src/godef

あれれ? ってんで、思い出した。自身にhelpを内蔵してるはず。

cent:~$ godef -h
usage: godef [flags] [expr]
  -A    print all type and members information
  -a    print public type and member information
  -acme
        use current acme window
  -debug
        debug mode
  -f string
        Go source filename
  -i    read file from stdin
  -json
        output location in JSON format (-t flag is ignored)
  -o int
        file offset of identifier in stdin (default -1)
  -t    print type information

もう少し詳しいのは、やっぱりgodocだろうね。 godoc -http :6060

 Godef prints the source location of definitions in Go programs.

Usage:

godef [-t] [-a] [-A] [-o offset] [-i] [-f file][-acme] [expr]

File specifies the source file in which to evaluate expr. Expr must be an
identifier or a Go expression terminated with a field selector.

If expr is not given, then offset specifies a location within file, which
should be within, or adjacent to an identifier or field selector.

If the -t flag is given, the type of the expression will also be printed.
The -a flag causes all the public members (fields and methods) of the expression,
and their location, to be printed also; the -A flag prints private members too.

If the -i flag is specified, the source is read from standard input,
although file must still be specified so that other files in the same
source package may be found.

If the -acme flag is given, the offset, file name and contents are read from
the current acme window.

例が出てた。

$ cd $GOROOT
$ godef -f src/pkg/xml/read.go 'NewParser().Skip'
src/pkg/xml/read.go:384:18
$

これを見ると、環境変数から勝手に検索とかは無い模様。

仕込み

godef側にどんな引数が渡ってくるか、調べてみよう。emacsから起動されるんで、ログを書き出す事にする。

func main() {
        argsWithProg := os.Args
        xf,_ := os.Create("/tmp/GODEF.log")
        defer xf.Close()
        fmt.Fprintf(xf, "%s\n", argsWithProg)
        xf.Sync()

        flag.Usage = func() {
           :

上記のように、mainの最初に5行追加した。os.Argsで、いわゆるargvの配列全部が得られる。後はそれをFprintfを使って、ファイルに書き出すだけ。こういうのは、スニペットから引いて来るのが定番だ。

早速、emacsってかgo-mode上で、godef-jumpさせてみる。

cent:godef$ cat /tmp/GODEF.log
[/home/sakae/go/bin/godef -i -t -f /home/sakae/go/src/dav/dav.go -o 364]
cent:godef$ cat /tmp/GODEF.log
[/home/sakae/go/bin/godef -i -t -f /home/sakae/go/src/dav/dav.go -o 696]

最初のログはwebdav.Handlerを探そうとした時。後のログは、http.ListenAndServeを探そうとした時。違いは、-o の後の数値だ。これって、カーソルがポイントするファイル先頭からのオフセットになるんだな。

emacsから渡ってくる引数には、-iが付いてるけど、これはファイル入力がStdinから取り込めって指令。今回は、それを省いて、結果を検索させてみる。

cent:godef$ /home/sakae/go/bin/godef -t -f /home/sakae/go/src/dav/dav.go -o 364
godef: no declaration found for webdav.Handler
cent:godef$ /home/sakae/go/bin/godef -t -f /home/sakae/go/src/dav/dav.go -o 696
/usr/local/go/src/net/http/server.go:3002:6
ListenAndServe func(addr string, handler Handler) error

最初の検索は失敗。二番目の方は、server.goの3002行目の6カラム目に、お目当ての探し物が有りましたって報告してる。

cent:godef$ cat /home/sakae/go/src/dav/dav.go | /home/sakae/go/bin/godef -t -i -o 696
/usr/local/go/src/net/http/server.go:3002:6
ListenAndServe func(addr string, handler Handler) error

余談になるけど、-iを活かしてStdinからコンテンツを流し込んであげれば、-fでのファイル指定は不要になる。各種アプリの都合に合わせて、どちらでも採用して下さいって魂胆みたいだ。

ともかく、今後の道筋は、godefを紐解いて行く事になるな。

godefをdlvの対象にする

ってんで、起動したらあえなくコンパイルエラー。go get でコンパイルするのとdlvが行うのでは環境が違うのかね? acme関係が軒並みエラーになった。幸いにもその機能は使っていないので、コメントにして殺した。

そして、まずはちゃんと検出してくれる例。

Run dlv (like this): dlv debug godef.go -- -t -f /home/sakae/go/src/dav/dav.go -o 696

こんな風に起動して、

 95//              fmt.Printf("\t%s:#%d\n", afile.name, afile.runeOffset)
 96//      }
 97=>      switch e := o.(type) {
 98        case *ast.ImportSpec:
 99                path := importPath(e)

解析が終わったぽい所で確認。

(dlv) p o
github.com/rogpeppe/godef/go/ast.Node(*github.com/rogpeppe/godef/go/ast.SelectorExpr) *{
        X: github.com/rogpeppe/godef/go/ast.Expr(*github.com/rogpeppe/godef/go/ast.Ident) *{
                NamePos: 687,
                Name: "http",
                Obj: *(*github.com/rogpeppe/godef/go/ast.Object)(0xc000178f00),},
        Sel: *github.com/rogpeppe/godef/go/ast.Ident {
                NamePos: 692,
                Name: "ListenAndServe",
                Obj: *github.com/rogpeppe/godef/go/ast.Object nil,},}

ちゃんと、お目当てのものが、それっぽく捉えられている。

今度は、ファイルオフセットを364にセットして、同一の所で確認すると、

(dlv) p o
github.com/rogpeppe/godef/go/ast.Node(*github.com/rogpeppe/godef/go/ast.SelectorExpr) *{
        X: github.com/rogpeppe/godef/go/ast.Expr(*github.com/rogpeppe/godef/go/ast.Ident) *{
                NamePos: 356,
                Name: "webdav",
                Obj: *(*github.com/rogpeppe/godef/go/ast.Object)(0xc0001798b0),},
        Sel: *github.com/rogpeppe/godef/go/ast.Ident {
                NamePos: 363,
                Name: "Handler",
                Obj: *github.com/rogpeppe/godef/go/ast.Object nil,},}

ちゃんと期待に沿うようなデータが取れてるなと思って、先に進むと

122                if obj, typ := types.ExprType(e, types.DefaultImporter, types.FileSet); obj != nil {
123                        done(obj, typ)
124                }
125=>              fail("no declaration found for %v", pretty{e})

122行がfalseになって、すっ飛ばされていた。eは、上のoと同一だったので、惜しい失敗をしてるな。

ちゃんと答えを返してくる方だと、言うまでもなく、112,123行と言う流れになってた。

以上が、外観上の流れだ。これじゃ何処に原因が有るか分からないので、基礎から積み上げるしかないな。

ast

ソースをざっと見してきた経験から、どうもast(抽象構文木)って言う技法が使われているっぽい。

Go静的解析ハンズオン

Go言語で書いたソースコードを解析する さっと解析して見せてくれるのは最高

GoのためのGoって、goのためのgoパッケージ群なのね。詳しい説明ありがとう。

Goの抽象構文木(AST)を手入力してHello, Worldを作る 文字通り、コンパイラーになった積りで、例のやつを組み立てると言う、斜めに構えた記事

ASTを取得する方法を調べる

godef -debug

godefにdebugなんてオプションが有るので試してみる。

cent:~$ godef -debug -t -f /home/sakae/go/src/dav/dav.go -o 696
exprType tuple:false pkg: *ast.SelectorExpr http.ListenAndServe [
exprType tuple:false pkg: *ast.Ident http [
exprType tuple:false pkg: *ast.ImportSpec "net/http" [
] -> 0x0, Type{package "" *ast.ImportSpec "net/http"}
] -> 0xc00018ef50, Type{package "" *ast.ImportSpec "net/http"}
member Type{package "" *ast.ImportSpec "net/http"} 'ListenAndServe' {
} -> &{func ListenAndServe 0xc00063b860 <nil> <nil>}
exprType tuple:false pkg: *ast.Ident ListenAndServe [
exprType tuple:false pkg: *ast.FuncType func(addr string, handler Handler) error [
] -> 0x0, Type{type "" *ast.FuncType func(addr string, handler Handler) error}
] -> 0xc0005c2f50, Type{func "" *ast.FuncType func(addr string, handler Handler) error}
] -> 0xc0005c2f50, Type{func "net/http" *ast.FuncType func(addr string, handler Handler) error}
/usr/local/go/src/net/http/server.go:3002:6
ListenAndServe func(addr string, handler Handler) error

これ、正しく検索出来た方。

cent:~$ godef -debug -t -f /home/sakae/go/src/dav/dav.go -o 364
exprType tuple:false pkg: *ast.SelectorExpr webdav.Handler [
exprType tuple:false pkg: *ast.Ident webdav [
exprType tuple:false pkg: *ast.ImportSpec "golang.org/x/net/webdav" [
] -> 0x0, Type{package "" *ast.ImportSpec "golang.org/x/net/webdav"}
] -> 0xc00018f900, Type{package "" *ast.ImportSpec "golang.org/x/net/webdav"}
member Type{package "" *ast.ImportSpec "golang.org/x/net/webdav"} 'Handler' {
    /home/sakae/go/src/golang.org/x/net/webdav/prop.go:111:40: expected '}', found ':'
    /home/sakae/go/src/golang.org/x/net/webdav/prop.go:112:3: expected declaration, found 'IDENT' findFn
  :
} -> <nil>
] -> 0x0, Type{bad "" <nil> }
godef: no declaration found for webdav.Handler

検索に失敗した方。どうも、期待してた文法と合っていない風だ。という事は、godef自体が想定していない検索をやらかしている感じがするな。

このdebugって、何処で出しているのだろう?

go/types/types.go

func (ctxt *exprTypeContext) exprType(n ast.Node, expectTuple bool, pkg string) (xobj *ast.Object, typ Type) {
        debugp("exprType tuple:%v pkg:%s %T %v [", expectTuple, pkg, n, pretty{n})
        defer func() {
                debugp("] -> %p, %v", xobj, typ)
        }()
        switch n := n.(type) {
          :

debugpの最後に付けてあるpから、これを書いた人はlisperって認定するよ。そう思えば、debugと言いつつ、traceですな。関数に入った時に、どんな引数で呼ばれたか、関数から退去する時の返値はどうか、表示する。退去はあちこちからreturnするんだけど、deferを使ってる。これ、座布団3枚のgoらしいコードだな。

func debugp(f string, a ...interface{}) {
        if Debug {
                log.Printf(f, a...)
        }
}

debugpの実体。logを使ってるけど、これだと(オイラーには無用な)日時情報が頭に付いちゃうので、fmtを使うと吉。

そして、traceの結果にさりげなくmemberなんてのが混じっていた。懐かしくなって、思わずgoshを動かしちゃったぞ。

gosh> (define lang (list 'perl 'ruby 'python 'c 'go 'java))
lang
gosh> lang
(perl ruby python c go java)
gosh> (member 'go lang)
(go java)
gosh> (member 'basic lang)
#f

小さくする

debugって語句で検索してみると、godef.goの他に、サブとなるgo/paser/parser.go とかにも散見される。この際なんで、godef.goだけを取り出してきて、ソースを小さくしてから追ってみる。

サブパッケージのgo以下は、本家の物から改変されてる(と思う)ので、それはそのまま使う。 供試ファイルのdav.goもgodef.goに置いてからgo build したら、こいつもコンパイル対象になっちゃったよ。

って事で、上での疑問、go get と、go build godef.go から、go get の仕組みが分かったぞ。

git clone URL
cd xxx
go build
mv obj bin/ (or pkg/)  // Like go install

が、go get URL のやってる事なのね。go buildにファイル名を指定しないと、pwdにあるgoのソースを全部まとめてコンパイルする仕様なんだな。昔ちらっと読んだ記憶があったけど、蘇ってきたよ。

それで、小さくしたやつは下記ね。

func main() {
        tflag := true
        types.Debug = true
        fword := "http.Handle"      //      fword := "webdav.Handler"
        filename := "/home/sakae/go/src/dav/dav.go"

        var src []byte
        src, err := ioutil.ReadFile(filename)
        if err != nil {
                fail("cannot read %s: %v", filename, err)
        }
        pkgScope := ast.NewScope(parser.Universe)
        f, err := parser.ParseFile(types.FileSet, filename, src, 0, pkgScope, types.DefaultImportPathToName)
        if f == nil {
                fail("cannot parse %s: %v", filename, err)
        }
        var o ast.Node
        o = parseExpr(f.Scope, fword)
//      o = findIdentifier(f, searchpos)    //      searchpos := 696
        switch e := o.(type) {
        case *ast.ImportSpec:
         :
        case ast.Expr:
         :
                if obj, typ := types.ExprType(e, types.DefaultImporter, types.FileSet); obj != nil {
                        done(obj, typ)
                }
                fail("no declaration found for %v", pretty{e})

こうして、フラグ類を整理すると、構造がよく見えてくる。昔から本質はプリンター用紙1枚に圧縮せよって事でやってたけど、それの再現だな。指定されたファイルから一気読みする技が冴えてるね。

(dlv) p f
*github.com/rogpeppe/godef/go/ast.File {
        Doc: *github.com/rogpeppe/godef/go/ast.CommentGroup nil,
        Package: 1,
        Name: *github.com/rogpeppe/godef/go/ast.Ident {
                NamePos: 9,
                Name: "main",
                Obj: *github.com/rogpeppe/godef/go/ast.Object nil,},
        Decls: []github.com/rogpeppe/godef/go/ast.Decl len: 3, cap: 4, [
                ...,
                ...,
                ...,
        ],
        Scope: *github.com/rogpeppe/godef/go/ast.Scope {
                Outer: *(*github.com/rogpeppe/godef/go/ast.Scope)(0xc000060480),
                Objects: map[string]*github.com/rogpeppe/godef/go/ast.Object [...],},
        Imports: []*github.com/rogpeppe/godef/go/ast.ImportSpec len: 0, cap: 0, nil,
        Unresolved: []*github.com/rogpeppe/godef/go/ast.Ident len: 0, cap: 0, nil,
        Comments: []*github.com/rogpeppe/godef/go/ast.CommentGroup len: 0, cap: 0, nil,}

パースした結果。面白そうな所を掘り下げるてみる。

(dlv) p f.Decls
[]github.com/rogpeppe/godef/go/ast.Decl len: 3, cap: 4, [
        *github.com/rogpeppe/godef/go/ast.GenDecl {
                Doc: *github.com/rogpeppe/godef/go/ast.CommentGroup nil,
                TokPos: 15,
                Tok: IMPORT,
                Lparen: 22,
                Specs: []github.com/rogpeppe/godef/go/ast.Spec len: 5, cap: 8, [
                        ...,
                        ...,
                        ...,
                        ...,
                        ...,
                ],
                Rparen: 120,},
        *github.com/rogpeppe/godef/go/ast.GenDecl {
                Doc: *github.com/rogpeppe/godef/go/ast.CommentGroup nil,
                TokPos: 123,
                Tok: VAR,
                Lparen: NoPos,
                Specs: []github.com/rogpeppe/godef/go/ast.Spec len: 1, cap: 1, [
                        ...,
                ],
                Rparen: NoPos,},
        *github.com/rogpeppe/godef/go/ast.FuncDecl {
                Doc: *github.com/rogpeppe/godef/go/ast.CommentGroup nil,
                Recv: *github.com/rogpeppe/godef/go/ast.FieldList nil,
                Name: *(*github.com/rogpeppe/godef/go/ast.Ident)(0xc0001efda0),
                Type: *(*github.com/rogpeppe/godef/go/ast.FuncType)(0xc000200860),
                Body: *(*github.com/rogpeppe/godef/go/ast.BlockStmt)(0xc0001fe2d0),},
]

皮をむくとその中にまた包まれたものが有るって具合で、構造がよく分かっていないと、容易に目的地にたどり着けないな。

inside golang

Golang Internals, Part 1: Main Concepts and Project Structure

LiteIDEを起動しようとしたら、怒られた。 「 Windows8、Windows10でダウンロードしたプログラムを実行すると「WindowsによってPCが保護されました」と表示され実行できない 」をみて、解決。