dive into iota: iotaはいつ誰が管理しているのか?

December 08, 2019

目次

はじめに

この記事は、Go3 Advent Calendar 2019の9日目の記事です。

普段、私たちは何気なくiotaを利用しています。 しかし、私はiotaを使っていくうちに、「誰が1ずつインクリメントしてくれているんだろう…そもそもiotaってなんなんだろう…」と不思議に思うようになりました。

const (
  A = iota + 1 // 1
  B            // 2..
  C            // 3... どうやってインクリメントとされていくんだ?
)

そのことが気になった私は、iotaがどのようにコンパイラによって扱われるかを調べてみることにしました。

iota とは

念の為、iotaとは何か、おさらいしておきましょう。 iota は、定数に整数の連番を割り振るために用意された仕組みです。

type Weekday int

const (
    Sunday Weekday = iota // 0
    Monday                // 1
    Tuesday               // 2
    Wednesday             // 3
    Thursday              // 4
    Friday                // 5
    Saturday              // 6
)

const (
    Shinjuku int = iota // 0    (iota=0)
    Yoyogi              // 1    (iota=1)
    Shibuya      = 999  // 999  (iota=2)
    Harajuku            // 3    (iota=3)
)

const (
    A, B = iota  // 0, 0
    C, D, E      // 1, 1, 1
    F, G, H, I   // 2, 2, 2, 2
    J            // 3
)

そして、iota事前宣言済み識別子であり、型なしの数値定数です。 なぜiotaは定数であるにも関わらず、0から始まる整数の連番を返し、別のconstではまた0から始まるのでしょうか。

本題の前に、2つの前提

本章では、iotaの値がいつどのように決定されるかを説明するために、以下を前提知識として共有します。

  • Goコンパイラ gc によるコンパイルの流れ
  • 「定数の宣言」の文法と各名称

前提1: Goコンパイラ gc によるコンパイルの流れ

どのタイミングで iota の値が計算されるかを知るためには、コンパイルの流れを知る必要があります。

Goコンパイラgcは、大まかにいえば以下の手順に従ってコンパイルを行います。

  1. syntaxパッケージ
    1.1 ソースコードを字句リストへ分割
    1.2 字句リストを構文ツリーへ変換
  2. gcパッケージ
    2.1 構文ツリーからAST
    2.2 型チェック
    2.3 SSA(中間表現)へ変換
  3. ssaパッケージ
    3.1 SSAを最適化する
    3.2 機械語を生成

前提2: 「定数の宣言」の文法と各名称

本章では、以後の説明のために、「定数の宣言」の文法と各名称を示します。

「定数の宣言」の文法をEBNF形式で表現すると、以下の通りとなります。

ConstDecl      = "const" ( ConstSpec | "(" { ConstSpec ";" } ")" ) .
ConstSpec      = IdentifierList [ [ Type ] "=" ExpressionList ] .
IdentifierList = identifier { "," identifier } . // 識別子のリスト
ExpressionList = Expression { "," Expression } . // 式のリスト

つまり、定数の宣言において、それぞれの部分の名前は以下の通りです。 それぞれの名称は、このあとの説明で用いますので、覚えておいてください。

const (
  A, B, C IdentifierList = 1, 2, 3ExpressionListConstSpec
  D, E, F IdentifierList = "foo", "bar", "baz"ExpressionListConstSpec
)

「定数の宣言」がASTになるまで

ソースコードが構文ツリーになるまで

ここから、本題である「iotaがどのようにコンパイラによって扱われるか」を解説します。

結論からいえば、定数の宣言、つまりConstDeclおよびConstSpecが字句解析され、構文解析され、ASTへ変換される過程で、iotaのカウンタがいつ0へリセットされるか、各定数に対してどんな値を返すかが決定されます。

まず、syntaxパッケージによって、ソースコードが構文ツリーへ変換されるようすを見てみましょう。

  1. syntaxパッケージ
    1.1 ソースコードを字句リストへ分割
    1.2 字句リストを構文ツリーへ変換
  2. gcパッケージ
    2.1 構文ツリーからAST
    2.2 型チェック
    2.3 SSA(中間表現)へ変換
  3. ssaパッケージ
    3.1 SSAを最適化する
    3.2 機械語を生成

まず、gcは、.goファイルごとにsyntaxパッケージのParse関数を呼びます。 呼び出されたParse関数は、字句解析をした後、これを構文ツリーへ変換するためにfileOrNil関数を呼びます。 fileOrNil関数は、以下の流れに従って処理を行います。

  1. File構造体を初期化する
    File構造体は、そのファイルで定義されているimport、const、var、typeそしてfuncの一覧を保持するDeclListフィールドを持つ
  2. パッケージ名を取得する
  3. import文にて指定されているパッケージの一覧をimportDecl関数によって取得し、DeclListへ加える
  4. ファイルのトップレベルにある定数、変数、型そして関数の宣言を、それぞれconstDeclvarDecltypeDeclfuncDeclOrNilによって、Declインタフェースを持つ構造体ConstDeclVarDeclTypeDeclFuncDeclへ変換されます。

syntaxパッケージにおける関数の呼び出し
syntaxパッケージにおける関数の呼び出し

ConstDecl構造体は「ConstDecl」という名前ではありますが、実際には先程の文法でいうConstSpecを表しています(不思議ですね)。 また、ConstDecl構造体はGroupフィールドを持っていて、このGroupフィールドはそれぞれのConstSpecがどのConstDeclに属するかを保持します。たとえば、同じConstDeclの中で宣言されたConstSpecがConstDecl構造体に変換された時、Groupフィールドの値が同じになります。そして、他のConstDeclの中で宣言されたConstSpecが変換されたConstDecl構造体とは、Groupフィールドの値が異なります。

構文ツリーがASTになるまで

次に、gcパッケージによって、構文ツリーがASTへ変換されるようすを見てみましょう。

  1. syntaxパッケージ
    1.1 ソースコードを字句リストへ分割
    1.2 字句リストを構文ツリーへ変換
  2. gcパッケージ
    2.1 構文ツリーからAST
    2.2 型チェック
    2.3 SSA(中間表現)へ変換
  3. ssaパッケージ
    3.1 SSAを最適化する
    3.2 機械語を生成

gcパッケージは、syntaxパッケージによって得られたFile構造体を受け取り、Node構造体のツリーへ変換したのち、この木構造をたどって型チェックし、SSA(中間表現)へ変換します。

そして、このNode構造体は、Xoffsetという、色々な値を保存しておくためのフィールドを持っています。さらに、このXoffsetフィールドこそがiotaの値を保持します。実際に、gcパッケージのソースコードのNode構造体を定義する部分を読むと、コメントに

Named OLITERALs use it to store their ambient iota value. OCLOSURE uses it to store ambient iota value, if any.

とあります。

type Node struct {
      // Tree structure.
    // Generic recursive walks should follow these fields.
    Left  *Node
    Right *Node
    Ninit Nodes
    Nbody Nodes
    List  Nodes
    Rlist Nodes

    // ...

    // Various. Usually an offset into a struct. For example:
    // ...
    // - Named OLITERALs use it to store their ambient iota value.
    // ...
    // - OCLOSURE uses it to store ambient iota value, if any.
    // Possibly still more uses. If you find any, document them.
    Xoffset int64

また、Node構造体はSetIota関数を持ちます。この関数がNode構造体のXoffsetフィールドにiotaの値を代入しています。

func (n *Node) SetIota(x int64) {
    n.Xoffset = x
}

では、このSetIota関数は、どこから呼ばれるのでしょうか。 答えは、ConstDecl構造体をNode構造体へ変換する、constDecl関数です。 constDecl関数は、ConstDecl構造体へ変換されたConstSpecを引数に受け取り、これをNodeのリストへ変換します。 そして、ConstSpecが属するConstDeclが異なる場合、カウンタを0にリセットします。 加えて、Nodeのリストへの変換が終わったあと、カウンタをインクリメントします。

type constState struct {
    group  *syntax.Group
    typ    *Node
    values []*Node
    iota   int64
}

func (p *noder) constDecl(decl *syntax.ConstDecl, cs *constState) []*Node {
    // 最後に処理したConstSpecと今処理しているConstSpecの
    // Groupが異なる場合はiotaのカウンタをリセットする
    if decl.Group == nil || decl.Group != cs.group {
        *cs = constState{
            group: decl.Group,
        }
    }

    // そのCountSpecにて宣言された定数名の一覧を取得する
    names := p.declNames(decl.NameList)
    // ...

    nn := make([]*Node, 0, len(names))
    for i, n := range names {
        // ...
        // iotaのカウンタ値をiota値として渡す
        n.SetIota(cs.iota)

        nn = append(nn, p.nod(decl, ODCLCONST, n, nil))
    }

    // ...

    // iotaのカウンタをインクリメントする
    cs.iota++

    return nn
}

まとめ

以上より、iota の値は 構文解析の結果からASTが生成される時に、noder.constDecl関数によって決定されるものかと思われます。
本記事に間違いがございましたら、Twitterのリプライにてお知らせ下さると幸いです。 拙い記事ですが、ここまでお付き合い頂きありがとうございました。

参考記事

私について

岩佐 幸翠
2019年4月に株式会社ディー・エヌ・エーへ新卒として入社し、GoとVue.jsを書いています。もっと詳しく

よかったらこの記事をシェアしてください:
B!

© 2020 Kosui Iwasa a.k.a. ebiebievidence
サイトマップ

このページでは、Google Analyticsを利用しています。