1 of 30

静的解析と学ぶ

型パラメタ(ジェネリクス)

mercari.go #18

2022/01/20(木)

資料URL:https://tenn.in/typeparamanalysis

The Go gopher was designed by Renée French.

The gopher stickers was made by Takuya Ueda.

Licensed under the Creative Commons 3.0 Attributions license.

2 of 30

上田拓也

Go ビギナーズ

Go Conference

@tenntenn

tenntenn.dev

Google Developer Expert (Go)

一般社団法人 Gophers Japan 代表理事

tenntenn Conference主催

Experts Team

3 of 30

オンラインでセッションを見るときは

  • 登壇者(私)を孤独にさせないように!
    • YouTubeのコメントやTwitterなどのリアクションは大きめに!
    • 知らなかったことには「へぇ」
    • 知ってることには「そうそう、それ」
    • あなたの相槌で登壇者を孤独から救えます

4 of 30

書いたところ

  • はじめに
    • はじめにを書くと感慨深いです
  • 0.2 入出力
    • ioパッケージを中心にio/fsパッケージにも触れてます
  • 2.1 Nature Remoによる家電の操作
    • APIクライアントについて書いてます
  • 4.1 高度なテキスト変換
    • x/text/tranformパッケージについて書いてます

5 of 30

Go1.18 リリースパーティ

  • 型パラメタの詳細はこちらのイベントで!
    • 2022年02月18日(金) 19:00 〜
    • https://gocon.connpass.com/event/234198/
    • Remoを使って開催予定!
      • YouTube Liveでの配信はありません
      • アーカイブは残す予定です

6 of 30

こちらもどうぞ

7 of 30

静的解析についてはこちら

8 of 30

インターンシップで学ぶ

  • Go1.18が学べる短期インターシップ(5日間)
    • https://mercan.mercari.com/articles/31914/
    • 1/20 19時まで応募可
    • 学生向け
    • 時給:2,500円

9 of 30

今日話すこと

  • 型パラメタ(ジェネリクス)を含むコードの静的解析
    • 型パラメタの概要
    • Goにおける静的解析の概要
    • 型パラメタを含むコードの抽象構文木
    • 型パラメタを含むコードの型情報

10 of 30

型パラメタ

  • ジェネリックな型や関数を定義できる
    • 実際の型を使用する側が指定できる
    • 型制約としてインタフェースが使用できる
    • 型セットという概念が登場

var printStr func([]string) = Print[string]

printStr([]string{"Hello, ", "playground\n"})

Print[T]

Print[string]

func([]string)

Print[int]

func([]int)

T => string

T => int

インスタンス化

func Print[T any](s []T) {

for _, v := range s {

fmt.Print(v)

}

}

11 of 30

静的解析

  • 静的解析とは?
    • プログラムを実行せずに解析すること
    • ソースコードの構造や意味を解析する
    • 例:lint、コード補完、 コードフォーマッタ

11

コーディング

010100101000101000

コンパイル

010100101000101000

デプロイ

リリース

テスト

QA

監視

静的解析

12 of 30

Goにおける静的解析のフェーズ

  • 静的解析はいくつかのフェーズに分かれている
    • 後のフェーズにいくこと、さらに詳しい情報が手に入る
    • 各フェーズで手に入る情報を使い分けながら解析する
    • 各フェーズで手に入る情報の紐付けの仕方がキモ
      • このノードの型情報は?など

12

構文解析

型チェック

静的単一代入形式

ポインタ解析

13 of 30

静的解析に使えるパッケージ

  • 標準で用意されている(goパッケージ)
    • 準標準的なものもある(golang.org/x/tools/goパッケージ)

13

ast

抽象構文木(AST)を提供

build

パッケージに関する情報を集める

constant

定数に関する型を提供

doc

ドキュメントをASTから取り出す

format

コードフォーマッタ機能を提供

importer

コンパイラに適したImporterを提供

parser

構文解析 機能を提供

printer

AST 表示機能を提供

scanner

字句解析 機能を提供

token

トークンに関する型を提供

types

型チェックに関する機能を提供

analysis

静的解析ツールをモジュール化するパッケージ

ast

AST関連のユーティリティ

callgraph

call graph関連

cfg

control flow graph関連

expect

構造化されたコメントを処理する

packages

Go Modulesを前提としたパッケージ情報の収集から構文解析、�型チェックまでを行うパッケージ

pointer

ポインタ解析

ssa

Static Single Assignment (SSA) 関連

types

型情報関連

goパッケージのサブパッケージ

golang.org/x/tools/goパッケージのサブパッケージ

14 of 30

字句解析- go/scanner,go/token

  • 入力された文字列をトークンとして分解

14

IDENT

ADD

INT

トークン

ソースコード:

v + 1

15 of 30

構文解析 - go/parser,go/ast

  • トークンを抽象構文木(AST)に変換
    • AST: Abstract Syntax Tree

15

v + 1

IDENT

ADD

INT

ソースコード:

+

v

1

BinaryExpr

Ident

BasicLit

トークン:

抽象構文木(AST):

16 of 30

型チェック - go/types,go/constant

  • 型情報を抽象構文木から抽出
    • 識別子の解決
    • 型の推論
    • 定数の評価

16

n := 100 + 200

m := n + 300

定数の評価

= 300

型の推論

-> int

識別子の解決

17 of 30

静的解析ツールを簡単に作る

  • golang.org/x/tools/go/analysisパッケージ
    • 静的解析ツールのモジュール化を提供するパッケージ
    • Go1.12からgo vetでも使われるようになった
    • 構文解析と型チェックは自動で行う
    • Analyzer単位で開発する

var Analyzer = &analysis.Analyzer{

Name: "simple",

Doc: "simple is simple Analyzer",

Run: run,

Requires: []*analysis.Analyzer{inspect.Analyzer},

}

func run(pass *analysis.Pass) (interface{}, error) {

/* 解析処理 */

return nil, nil

}

依存するAnalyzer

抽象構文木や型情報を保持

18 of 30

skeleton

  • go/analysis用のスケルトンコードジェネレータ
    • https://github.com/gostaticanalysis/skeleton
    • 簡単に静的解析ツールを始めることができる
    • Analyzer、テストコード、main.goの雛形作ってくれる

$ skeleton myanalyzer

myanalyzer

├── cmd

│ └── myanalyzer

│ └── main.go

├── myanalyzer.go

├── myanalyzer_test.go

└── testdata

└── src

└── a

└── a.go

19 of 30

goパッケージの何が変わるのか

  • GOROOT/api/go1.18.txtを見てみよう

$ grep "go/" `go1.18beta1 env GOROOT`/api/go1.18.txt

pkg go/ast, method (*IndexListExpr) End() token.Pos

pkg go/ast, method (*IndexListExpr) Pos() token.Pos

pkg go/ast, type FuncType struct, TypeParams *FieldList

pkg go/ast, type IndexListExpr struct

pkg go/ast, type IndexListExpr struct, Indices []Expr

pkg go/ast, type IndexListExpr struct, Lbrack token.Pos

pkg go/ast, type IndexListExpr struct, Rbrack token.Pos

pkg go/ast, type IndexListExpr struct, X Expr

pkg go/ast, type TypeSpec struct, TypeParams *FieldList

pkg go/constant, method (Kind) String() string

(略)

20 of 30

抽象構文木をダンプして確かめる

  • ast.Print関数を使う
    • 第2引数のノードをダンプする関数
  • knsh14/astreeを使う(使いたい)
    • treeコマンドっぽく出すツール
    • まだ未対応だけどPRは出てるっぽい

ast.Print(pass.Fset, pass.Files[0])

21 of 30

抽象構文木から型パラメタを得る

  • 関数(ast.FuncType構造体)
    • 関数の型を表すノード
    • *ast.FieldList型のTypeParamsフィールドから取得できる
    • ast.Field構造体で同じ制約の型パラメタのリストを表す
      • [X, Y any, Z fmt.Stringer]だとX, Y anyZ fmt.Stringerの単位
      • Namesフィールドが型パラメタのスライス([]*ast.Ident型
      • Typeフィールドが型制約(ast.Expr型
  • 型(ast.TypeSpec構造体)
    • 型宣言のtypeキーワードより後の部分を表すノード
    • *ast.FieldList型のTypeParamsフィールドから取得できる

func Print[T any](s []T) {...}

type Vector[T any] []T

22 of 30

ノードから型情報の型パラメタを得る

  • 型パラメタの型情報をtypes.TypeParam構造体で表す
    • types.Typeインタフェースを実装
    • Constraintメソッドで制約が取得できる
  • 型パラメタを表す識別子から取得できる
    • 例:ast.FuncType構造体のTypeParamsフィールド
      • ast.Field構造体のNamesフィールドの要素から取得できる
    • (*types.Info).TypeOfメソッドを使う

23 of 30

型情報から型パラメタを取得

  • 関数(types.Signature型)
    • TypeParamsメソッドから取得できる
    • レシーバの型パラメタもRecvTypeParamsメソッドから取得可
  • 型(types.Named型)
    • TypeParamsメソッドから型パラメタを取得
    • TypeArgsメソッドから型引数が取得できる
  • 型パラメタを表すオブジェクト
    • types.TypeName構造体で表される

24 of 30

制約を取得する

  • (*types.TypeParam).Constraintメソッドで取得
    • *types.Interface型で表す
    • IsComparebleメソッドで比較かのうかどうか取得
      • 組み込みのcompareble制約(インタフェース)
      • 型セットがcompareble制約のものの部分集合の制約
    • IsImplicitメソッドで暗黙のインタフェースか取得
      • func f[T ~int]()のようにinterface{ ~int }を省略できる
    • IsMethodSetメソッドでメソッドセットのみか判定
      • 制約以外にも使えるかどうか分かる

25 of 30

型パラメタを持つ関数の呼び出し

  • 関数呼び出しはast.CallExpr構造体が表す
    • Funフィールドに呼び出す関数を表すノードを保持
    • 型引数が指定してあるかどうかで型が変わる
    • 型引数が1つの場合は*ast.IndexExpr型の値が入る
      • ast.IdexExpr構造体はスライスやマップでも使われる既存の型
      • 後方互換性のためにast.IdentListExpr型に統合されなかった
    • 型引数が2つ以上の場合は*ast.IndexListExpr型の値が入る
      • f[string, int]("hoge", 100)のような場合
    • 型引数なしで型推論をする場合は*ast.Ident型の値が入る
      • 関数呼び出しは型引数が省略できる
      • 抽象構文木だけでは実際の型引数が分からない

26 of 30

型パラメタのインスタンス化

  • types.Instantiate関数を用いる
    • types.Context構造体はインスタンス化された型情報などを持つ
      • 型チェック時のtypes.Config構造体で指定できる
    • 第2引数の型は型パラメタを持つ型
    • 第3引数は型引数のスライス
    • 第4引数はインスタンス化の検証を行うかどうか

Print[T]

Print[string]

func([]string)

Print[int]

func([]int)

T => string

T => int

インスタンス化

func Instantiate(ctxt *Context, orig Type, targs []Type, validate bool) (Type, error)

27 of 30

新しく導入されるトークン

  • ~(チルダ):token.TILDE定数
    • インタフェース要素に使える
    • interface{ ~string | int }のように記述できる

InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .

InterfaceElem = MethodElem | TypeElem .

MethodElem = MethodName Signature .

MethodName = identifier .

TypeElem = TypeTerm { "|" TypeTerm } .

TypeTerm = Type | UnderlyingType .

UnderlyingType = "~" Type .

28 of 30

制約のインタフェースを表すノード

  • ast.InterfaceType構造体で表す
    • *ast.FieldList型のMethodsフィールドがインタフェース要素
      • Methodsなのは後方互換のため
    • ~stringなどはast.UnaryExpr型(単項演算式)で表す
    • int | ~stringなどはast.BinaryExpr型(2項演算式)で表す

InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .

InterfaceElem = MethodElem | TypeElem .

MethodElem = MethodName Signature .

MethodName = identifier .

TypeElem = TypeTerm { "|" TypeTerm } .

TypeTerm = Type | UnderlyingType .

UnderlyingType = "~" Type .

29 of 30

デモ

30 of 30

まとめ

  • 型パラメタを含むコードも静的解析できる
    • 標準パッケージで静的解析ができるから言語仕様に追従してくれる
    • 既存の静的解析のエコシステムを壊さずに導入されている
    • これから増えてくるジェネリクスのコードに対応しよう
      • さっそく、go vetのprintf.Analyzerは対応している