21.Ubuntuでひな形を作る

21-02.Bison3.8.1でのC++

Bison3.8.1でのC++の特徴
 前項では簡単な計算機を作成しました。Bisonのコード(parser.yy)を見ると、例えば【1】WindowsでC言語を作るで説明したようなBisonファイルとは、かなり感じが違うと思います。この項では、ソースコードを細かく見ていくことによって、どういう仕組みでParserを作成するのかを見てみたいと思います。  もちろん、このサンプルはバイソンのマニュアルに書いてあるものを作成したので、ドキュメントを詳しく読めばわかると思いますが、英語で書かれているので、日本語で理解するためには翻訳などをしながら読み進める必要がありますし、また、C++の使い方も高度な部分もあるので、そのあたりを補足しながら説明したいと思います。
 また、FlexおよびBison.lファイルや.yyファイルをもとに、C++のソースコードを作り出します。コンパイル(ビルド)はこのように、生成されたファイルが重要ですのでその中身も検討します。
calcCpp.ccと、その関連
 ではまずmain関数があります、calcCpp.ccから見ていきましょう。
 まずはおさらいです。calcCpp.ccは以下の内容になっています。
#include <iostream>
#include "driver.hh"

int
main (int argc, char *argv[])
{
  int res = 0;
  driver drv;
  for (int i = 1; i < argc; ++i)
    if (argv[i] == std::string ("-p"))
      drv.trace_parsing = true;
    else if (argv[i] == std::string ("-s"))
      drv.trace_scanning = true;
    else if (!drv.parse (argv[i]))
      std::cout << drv.result << '\n';
    else
      res = 1;
  return res;
}
 冒頭に
#include <iostream>
#include "driver.hh"
 という記述があります。iostreamはSTLのコンテナで、std::coutなどを保持します。
 driver.hh先ほど記述したヘッダファイルですね。続いてmain()関数ですが、forループの中では、コマンドラインの入力(コマンド引数)を見て、それによってParserの動きを変えています。
 ループが
  for (int i = 1; i < argc; ++i)
 のように1から始まっているのは、argv[0]には、プログラム名、すなわち、calcCppが入っているので、ここは使わないからです。
 コマンド引数に、-pがあればdrv.trace_parsingtrueにします。-sがあれば、drv.trace_scanningtrueにします。
 それ以外の場合はファイル名が入力された、と仮定して、drv.parse (argv[i])driverクラスのparse関数を呼び出します。
 何もコマンド引数が入力されなければ1を返して終了します。
 -p-sを確認する場合は、実行のコマンドを
./calcCpp -p test.txt
 と実行してみましょう。パーサーの動きをトレースして出力します。-sも追加すればスキャナーの動きもトレースします(-sだけつけても問題ありません)。
 ではdrv.parse (argv[i])では何をやっているか見てみましょう。driver.ccにその実体があります。
int
driver::parse (const std::string &f)
{
  file = f;
  location.initialize (&file);
  scan_begin ();
  yy::parser parse (*this);
  parse.set_debug_level (trace_parsing);
  int res = parse ();
  scan_end ();
  return res;
}
 fileという変数はdriverクラスのメンバ変数です。std::string型で、f(すなわちファイル名)を代入します。
 その後location.initializeという関数を読んでますが、locationは、driverクラスのメンバ変数でyy::location locationとヘッダで宣言されています。
これはbisonが作り出すlocation.hhに記述があります。
    /// Initialization.
    void initialize (filename_type* f = YY_NULLPTR,
                     counter_type l = 1,
                     counter_type c = 1)
    {
      begin.initialize (f, l, c);
      end = begin;
    }
 このように記述されています。locationの初期化です。
 ここで、yy::locationyy::parserのようにyyがついていますが、これはbisonが作り出すクラスのnamespaceです。
 別名のnamespaceにすることもできますが、このサンプルではやってないです。
 scan_begin ()とscan_end ()driverクラスのメンバ関数ですが、実体はdriver.ccではなくscanner.lに記述されています。
void
driver::scan_begin ()
{
  yy_flex_debug = trace_scanning;
  if (file.empty () || file == "-")
    yyin = stdin;
  else if (!(yyin = fopen (file.c_str (), "r")))
  {
    std::cerr << "cannot open " << file << ": " << strerror (errno) << '\n';
    exit (EXIT_FAILURE);
  }
}

void
driver::scan_end ()
{
  fclose (yyin);
}
 この場所に記述したわけは、yy_flex_debug、yyinといったflexのインターフェイスにアクセスする必要があるからです。
 この2つの関数はdriver::parse関数内で呼ばれています。
 さて、driver::parse関数には、少し難解な記述があります。
  int res = parse ();
 の記述です。parseというのはyy::parser parse (*this);で作成されるので、yy::parserのインスタンスのはずです。インスタンスに対してparse ();というのはC++の文法上許されるのでしょうか。
 実は、yy::parserクラスではoperator()という形で、演算子の多重定義を行っています。以下parser.cc(427行付近)
  int
  parser::operator() ()
  {
    return parse ();
  }
 という形でparserクラスのparse関数を呼び出しています。これはparse処理の関数bisonの重要な関数の一つです。それをdriver::parse関数内で呼び出す場合、parse.parse ()のような記述ではなく、単純にparse ()と呼ぶほうが、わかりやすい、ということなんでしょう。
driver.ccと、その関連
 driver.hhdriver.ccは、bisonとflexコンテンツ側の懸け橋となるクラスです。
 サンプルの状態だとわかりにくいかもしれないので、いくつか仕掛けをして確認したいと思います。
 まずdriver.hhdriverクラスに以下のようにcreate_prus関数を追加します。
class driver
{
public:
  driver ();

  std::map<std::string, int> variables;
  void create_prus(int a ,int b){
    std::cout << a << "+" << b << std::endl;
  }

//...中略
 続いてparser.yy60行付近
//...中略
exp:
  "number"
| "identifier"  { $$ = drv.variables[$1]; }
| exp "+" exp   { $$ = $1 + $3; }
| exp "-" exp   { $$ = $1 - $3; }
| exp "*" exp   { $$ = $1 * $3; }
| exp "/" exp   { $$ = $1 / $3; }
| "(" exp ")"   { $$ = $2; }
%%
//...中略
 となっている部分を
//...中略
exp:
  "number"
| "identifier"  { $$ = drv.variables[$1]; }
| exp "+" exp   { 
    drv.create_prus($1,$3);
    $$ = $1 + $3; 
  }
| exp "-" exp   { $$ = $1 - $3; }
| exp "*" exp   { $$ = $1 * $3; }
| exp "/" exp   { $$ = $1 / $3; }
| "(" exp ")"   { $$ = $2; }
%%
//...中略
 のように修正します。その後makeでビルドし
./calcCpp test.txt
 を実行すると
1+6
49
 と出力されます。つまり足し算が発生するところでdrv.create_prus($1,$3);が実行されるわけです。
 これの意味するところは、Parserであるparser.yyの構文解析の部分で、driverクラスのメンバ関数に簡単にアクセスできるということです。
 このサンプルではparser.yy内で計算を行っていますが、上から順番に実行される(順次実行)の状態ならいいのですが、分岐とかループを実装しようとすると、うまくいきません。
 つまりは、test.txtの内容をいったんどこか(メモリ上)に保存しておき、実際の実行は、その保存領域で実行します。
 このどこかに保存する行為こそがコンパイル(翻訳)というわけです。
 とりあえず、この段階ではコンパイル(翻訳)は実装せずに、元に戻しましょう。
 このようにparser.yyからはdriverクラスのメンバ関数には容易にアクセスできるので、今後はその辺を見据えながら説明します。
parser.yyと、その関連
 さていよいよ本丸です。parser.yy言語を作るための道具です。様々な機能があり、またBison3.8.1になって、強化された部分、そしてC++で利用するためのいろんな機能が入っています。
 ではparser.yyをおさらいしてみましょう。以下がその内容です。
%skeleton "lalr1.cc" // -*- C++ -*-
%require "3.8.1"
%header

%define api.token.raw

%define api.token.constructor
%define api.value.type variant
%define parse.assert

%code requires {
  # include <string>
  class driver;
}

// The parsing context.
%param { driver& drv }

%locations

%define parse.trace
%define parse.error detailed
%define parse.lac full

%code {
# include "driver.hh"
}

%define api.token.prefix {TOK_}
%token
  ASSIGN  ":="
  MINUS   "-"
  PLUS    "+"
  STAR    "*"
  SLASH   "/"
  LPAREN  "("
  RPAREN  ")"
;

%token <std::string> IDENTIFIER "identifier"
%token <int> NUMBER "number"
%nterm <int> exp

%printer { yyo << $$; } <*>;

%%
%start unit;
unit: assignments exp  { drv.result = $2; };

assignments:
  %empty                 {}
| assignments assignment {};

assignment:
  "identifier" ":=" exp { drv.variables[$1] = $3; };

%left "+" "-";
%left "*" "/";
exp:
  "number"
| "identifier"  { $$ = drv.variables[$1]; }
| exp "+" exp   { $$ = $1 + $3; }
| exp "-" exp   { $$ = $1 - $3; }
| exp "*" exp   { $$ = $1 * $3; }
| exp "/" exp   { $$ = $1 / $3; }
| "(" exp ")"   { $$ = $2; }
%%

void
yy::parser::error (const location_type& l, const std::string& m)
{
  std::cerr << l << ": " << m << '\n';
}
 それでは先頭から見ていきましょう。
%skeleton "lalr1.cc" // -*- C++ -*-
%require "3.8.1"
%header

%define api.token.raw

%define api.token.constructor
%define api.value.type variant
%define parse.assert
 この部分はBisonのバージョンとか、環境設定をするところです。
 ここで、今、重要なのは%define api.value.type variantの部分です。この記述によりunionを使わなくて済みます。つまりは、自由度の高いを扱えるようになります。
 続いて出てくるのが
%code requires {
  # include <string>
  class driver;
}
 です。ここではC/C++のインクルードファイルなどを記述できます。%codeというのはC/C++のコードを書くエリア、という意味です。class driver;というのはクラス宣言ですね。これからdriverクラスが出てくるので、覚えておけよ、みたいな意味です。
 続く
// The parsing context.
%param { driver& drv }
 で、その変数が宣言されます。今後このdrvparser.yyで使うことができるようになります。
 その後の
%locations

%define parse.trace
%define parse.error detailed
%define parse.lac full
 は、今はあんまり重要ではないです。locationを意識する場合は今後意味が出てくるかと思いますが、ここは流しましょう。
 続いて
%code {
# include "driver.hh"
}
 は重要です。ここでdriverクラスの宣言が読み込まれます。
 続く
%define api.token.prefix {TOK_}
%token
  ASSIGN  ":="
  MINUS   "-"
  PLUS    "+"
  STAR    "*"
  SLASH   "/"
  LPAREN  "("
  RPAREN  ")"
;
 ですが、実はここでASSIGN、MINUS、PLUSといったparser内の識別子が":="などで使えるようになります。結果として
exp "+" exp   { $$ = $1 + $3; }
 のような記述ができるのです。わかりやすいですね。これがないと
exp PLUS exp   { $$ = $1 + $3; }
 のように書くことになります。
 続く
%token <std::string> IDENTIFIER "identifier"
%token <int> NUMBER "number"
%nterm <int> exp
 は%define api.value.type variantの記述で書けるようになります。
 ここで非終端記号終端記号について説明します。
 例えば、ASSIGN、MINUS、PLUSなどのトークンは変化することがありませんね。一回PLUSというトークンで初期化されれば、以降PLUSあるいは"+"は同じ記号を指します。こういうのは終端記号と言って%tokenで定義します。
 しかしexpは違います。"number"がセットされたり、"identifier"がセットされたりして、それもまたexpにまとめられますね。
 こういう、どんどん変化していく(まとめられていく)トークンを非終端記号といいます。
 非終端記号は、前のBisonのバージョンでは%typeを使いました。expならば
%type <int> exp
 のような書き方ですね。新しいバージョンでも%typeは使えるのですが、実は%type終端記号にも使えたりします。それでは安全性がけけるということで%ntermができたらしいです。ですから
%type <int> exp
 と記述して、再コンパイルしても問題なくビルドできます。
 続く
%printer { yyo << $$; } <*>;
 は出力用の定義です。
 続くブロックが構文解析の規則を記述します。
%%
%start unit;
unit: assignments exp  { drv.result = $2; };

assignments:
  %empty                 {}
| assignments assignment {};

assignment:
  "identifier" ":=" exp { drv.variables[$1] = $3; };

%left "+" "-";
%left "*" "/";
exp:
  "number"
| "identifier"  { $$ = drv.variables[$1]; }
| exp "+" exp   { $$ = $1 + $3; }
| exp "-" exp   { $$ = $1 - $3; }
| exp "*" exp   { $$ = $1 * $3; }
| exp "/" exp   { $$ = $1 / $3; }
| "(" exp ")"   { $$ = $2; }
%%
 これがbisonの本体ともいえるでしょう。構文解析(parser)の部分で、字句解析(scanner)であるscanner.l(flex向けファイル)によってトークン分けされた部品を、どういう並びならどういう処理をするということを決めます。
 また優先順位も重要です。bisonは下のほうが優先順位が高くなります。ですので下から説明します。
 書式については
result: components...
        ;
 というものですが、あまりに分かりにくいのでもう少し詳しく説明します。
 resultというのは結果ですね。その名の通り$$にあたる部分の、多くの場合は非終端記号で定義された記号を記述します。
 上記のexpの規則を例にとりましょう。例えば
exp: "number"
 だけが規則化されていたとします。すると
expには "number"を入れることができる
 みたいな意味になります。これは定義部が省略されていて、省略しないで書くと
exp: "number" { $$ = $1 }
 という意味になります。定義しないと自動的にresultに、一番目のトークン(つまりは$1)が渡されます。
 規則は複数書くことができて
exp:
  "number"
| "identifier"  { $$ = drv.variables[$1]; }
| exp "+" exp   { $$ = $1 + $3; }
...以下略
 みたいに|記号でつなげられます。またはに意味ですね。
 また
exp "+" exp
 のトークン並びは
$1 $2 $3
 となるので$1 + $3という足し算をして、$$に渡すことになります。
 基本的にこの書式で書いていき、下位の定義が、上位にまとまられていく感じになります。
 またexpのブロックには;(セミコロン)がついてないですが、これは、構文解析規則の終わりを示す%%が直後にあるので省略できる、ということです。
 この上に記述がある
%left "+" "-";
%left "*" "/";
 というのは演算子の優先順位を指定するものです。
 例えば
a + b + c
というのは
(a + b) + c
 という意味ですよね。
 しかし代入演算子右優先です。C言語の=を代入演算子とした場合
a = b + c
 は
a = (b + c)
 という意味です。決して
(a = b) + c
 にはなりませんね。
 このようにして構文解析規則が、下位のブロックのresultが上位のブロックの一部になり、ということで、最終的に
unit: assignments exp  { drv.result = $2; };
 までたどり着きます。
 ところで、各ブロックのunit、assignments、assignmentは、resultですが、非終端記号ではありませんね(%ntermあるいは%typeで定義されていない)。
 これはflexから渡されたトークンでないからですがBisonではこのようにresultを自由に作成できます。要は、ツリー構造に矛盾なくつながればよいということなのでしょう。
 最後に、
%start unit;
 ですが、これは構文解析をどこからやるかの指定です。unitブロックからやりなさいという意味ですが、実際には、unitから、スクリプトソースを下に降りて行って、expブロックにたどり着き、そこから上に登っていくイメージですね。
scanner.lと、その関連
 最後にflex用のファイルであるscanner.lを見てみましょう。まずおさらいとして全コードを示します。
%{ /* -*- C++ -*- */
# include <cerrno>
# include <climits>
# include <cstdlib>
# include <cstring> // strerror
# include <string>
# include "driver.hh"
# include "parser.hh"
%}

%option noyywrap nounput noinput batch debug

%{
  // A number symbol corresponding to the value in S.
  yy::parser::symbol_type
  make_NUMBER (const std::string &s, const yy::parser::location_type& loc);
%}

id    [a-zA-Z][a-zA-Z_0-9]*
int   [0-9]+
blank [ \t\r]


%{
  // Code run each time a pattern is matched.
  # define YY_USER_ACTION  loc.columns (yyleng);
%}
%%
%{
  // A handy shortcut to the location held by the driver.
  yy::location& loc = drv.location;
  // Code run each time yylex is called.
  loc.step ();
%}
{blank}+   loc.step ();
\n+        loc.lines (yyleng); loc.step ();
"-"        return yy::parser::make_MINUS  (loc);
"+"        return yy::parser::make_PLUS   (loc);
"*"        return yy::parser::make_STAR   (loc);
"/"        return yy::parser::make_SLASH  (loc);
"("        return yy::parser::make_LPAREN (loc);
")"        return yy::parser::make_RPAREN (loc);
":="       return yy::parser::make_ASSIGN (loc);

{int}      return make_NUMBER (yytext, loc);
{id}       return yy::parser::make_IDENTIFIER (yytext, loc);
.          {
             throw yy::parser::syntax_error
               (loc, "invalid character: " + std::string(yytext));
}
<<EOF>>    return yy::parser::make_YYEOF (loc);
%%
yy::parser::symbol_type
make_NUMBER (const std::string &s, const yy::parser::location_type& loc)
{
  errno = 0;
  long n = strtol (s.c_str(), NULL, 10);
  if (! (INT_MIN <= n && n <= INT_MAX && errno != ERANGE))
    throw yy::parser::syntax_error (loc, "integer is out of range: " + s);
  return yy::parser::make_NUMBER ((int) n, loc);
}

void
driver::scan_begin ()
{
  yy_flex_debug = trace_scanning;
  if (file.empty () || file == "-")
    yyin = stdin;
  else if (!(yyin = fopen (file.c_str (), "r")))
  {
    std::cerr << "cannot open " << file << ": " << strerror (errno) << '\n';
    exit (EXIT_FAILURE);
  }
}

void
driver::scan_end ()
{
  fclose (yyin);
}
 上から順番に見ていきましょう。
 まず、
%{ /* -*- C++ -*- */
# include <cerrno>
# include <climits>
# include <cstdlib>
# include <cstring> // strerror
# include <string>
# include "driver.hh"
# include "parser.hh"
%}
 ですが
%{ と %} で囲む
 ことで、C/C++ブロックを記述することができます。ここでは必要なファイルをインクルードしています。
 続く
%option noyywrap nounput noinput batch debug
 は、オプションの設定です。
 今回のcalcCppには#includeのような機能がないため、yywrapは必要ありません。unput関数とinput関数も必要ありません。実際のファイルを解析します。これは、ユーザーとの対話型セッションではありません。最後に、スキャナートレースを有効にします。
 続く
%{
  // A number symbol corresponding to the value in S.
  yy::parser::symbol_type
  make_NUMBER (const std::string &s, const yy::parser::location_type& loc);
%}
 のmake_NUMBERは、数値を示す文字列をNUMBERトークンに変換するのに便利です。
 ここの記述は宣言ですが、実体は、下のほうにあります。その内容は
yy::parser::symbol_type
make_NUMBER (const std::string &s, const yy::parser::location_type& loc)
{
  errno = 0;
  long n = strtol (s.c_str(), NULL, 10);
  if (! (INT_MIN <= n && n <= INT_MAX && errno != ERANGE))
    throw yy::parser::syntax_error (loc, "integer is out of range: " + s);
  return yy::parser::make_NUMBER ((int) n, loc);
}
 となります。
 コードをよく読むとconst std::string &sから、strtol関数を使って数値に変換しています。その後
  return yy::parser::make_NUMBER ((int) n, loc);
 とparserの同名の関数を呼んでいます。これは、Bisonが作り出すparser.hhに実体があり
#if 201103L <= YY_CPLUSPLUS
      static
      symbol_type
      make_NUMBER (int v, location_type l)
      {
        return symbol_type (token::TOK_NUMBER, std::move (v), std::move (l));
      }
#else
      static
      symbol_type
      make_NUMBER (const int& v, const location_type& l)
      {
        return symbol_type (token::TOK_NUMBER, v, l);
      }
#endif
 とC++11かそれ以前で動きが異なっていますが、ようはintsymbol_typeにキャストしているわけですね。
 ここでsymbol_typeとは何ぞや、と調べるとparser.hhに保存されています。
    /// "External" symbols: returned by the scanner.
    struct symbol_type : basic_symbol<by_kind>
    {
      /// Superclass.
      typedef basic_symbol<by_kind> super_type;

      /// Empty symbol.
      symbol_type () YY_NOEXCEPT {}

      /// Constructor for valueless symbols, and symbols from each type.
#if 201103L <= YY_CPLUSPLUS
      symbol_type (int tok, location_type l)
        : super_type (token_kind_type (tok), std::move (l))
#else
      symbol_type (int tok, const location_type& l)
        : super_type (token_kind_type (tok), l)
#endif
      {
#if !defined _MSC_VER || defined __clang__
        YY_ASSERT (tok == token::TOK_YYEOF
                   || (token::TOK_YYerror <= tok && tok <= token::TOK_RPAREN));
#endif
      }
#if 201103L <= YY_CPLUSPLUS
      symbol_type (int tok, int v, location_type l)
        : super_type (token_kind_type (tok), std::move (v), std::move (l))
#else
      symbol_type (int tok, const int& v, const location_type& l)
        : super_type (token_kind_type (tok), v, l)
#endif
      {
#if !defined _MSC_VER || defined __clang__
        YY_ASSERT (tok == token::TOK_NUMBER);
#endif
      }
#if 201103L <= YY_CPLUSPLUS
      symbol_type (int tok, std::string v, location_type l)
        : super_type (token_kind_type (tok), std::move (v), std::move (l))
#else
      symbol_type (int tok, const std::string& v, const location_type& l)
        : super_type (token_kind_type (tok), v, l)
#endif
      {
#if !defined _MSC_VER || defined __clang__
        YY_ASSERT (tok == token::TOK_IDENTIFIER);
#endif
      }
    };
 このクラスは構造体となっていてbasic_symbolの派生クラスです。
 basic_symbolは、同様にparser.hhにありテンプレートクラスになっています。
    template <typename Base>
    struct basic_symbol : Base
    {
      /// Alias to Base.
      typedef Base super_type;

      /// Default constructor.
      basic_symbol () YY_NOEXCEPT
        : value ()
        , location ()
      {}

#if 201103L <= YY_CPLUSPLUS
      /// Move constructor.
      basic_symbol (basic_symbol&& that)
        : Base (std::move (that))
        , value ()
        , location (std::move (that.location))
      {
        switch (this->kind ())
    {
      case symbol_kind::S_NUMBER: // NUMBER
      case symbol_kind::S_exp: // exp
        value.move< int > (std::move (that.value));
        break;

      case symbol_kind::S_IDENTIFIER: // IDENTIFIER
        value.move< std::string > (std::move (that.value));
        break;

      default:
        break;
    }

      }
#endif

      /// Copy constructor.
      basic_symbol (const basic_symbol& that);

      /// Constructors for typed symbols.
#if 201103L <= YY_CPLUSPLUS
      basic_symbol (typename Base::kind_type t, location_type&& l)
        : Base (t)
        , location (std::move (l))
      {}
#else
      basic_symbol (typename Base::kind_type t, const location_type& l)
        : Base (t)
        , location (l)
      {}
#endif

#if 201103L <= YY_CPLUSPLUS
      basic_symbol (typename Base::kind_type t, int&& v, location_type&& l)
        : Base (t)
        , value (std::move (v))
        , location (std::move (l))
      {}
#else
      basic_symbol (typename Base::kind_type t, const int& v, const location_type& l)
        : Base (t)
        , value (v)
        , location (l)
      {}
#endif

#if 201103L <= YY_CPLUSPLUS
      basic_symbol (typename Base::kind_type t, std::string&& v, location_type&& l)
        : Base (t)
        , value (std::move (v))
        , location (std::move (l))
      {}
#else
      basic_symbol (typename Base::kind_type t, const std::string& v, const location_type& l)
        : Base (t)
        , value (v)
        , location (l)
      {}
#endif

      /// Destroy the symbol.
      ~basic_symbol ()
      {
        clear ();
      }



      /// Destroy contents, and record that is empty.
      void clear () YY_NOEXCEPT
      {
        // User destructor.
        symbol_kind_type yykind = this->kind ();
        basic_symbol<Base>& yysym = *this;
        (void) yysym;
        switch (yykind)
        {
       default:
          break;
        }

        // Value type destructor.
switch (yykind)
    {
      case symbol_kind::S_NUMBER: // NUMBER
      case symbol_kind::S_exp: // exp
        value.template destroy< int > ();
        break;

      case symbol_kind::S_IDENTIFIER: // IDENTIFIER
        value.template destroy< std::string > ();
        break;

      default:
        break;
    }

        Base::clear ();
      }

      /// The user-facing name of this symbol.
      const char *name () const YY_NOEXCEPT
      {
        return parser::symbol_name (this->kind ());
      }

      /// Backward compatibility (Bison 3.6).
      symbol_kind_type type_get () const YY_NOEXCEPT;

      /// Whether empty.
      bool empty () const YY_NOEXCEPT;

      /// Destructive move, \a s is emptied into this.
      void move (basic_symbol& s);

      /// The semantic value.
      value_type value;

      /// The location.
      location_type location;

    private:
#if YY_CPLUSPLUS < 201103L
      /// Assignment operator.
      basic_symbol& operator= (const basic_symbol& that);
#endif
    };
 このテンプレートクラスの説明は詳しくはしませんが、ようは、flex字句解析で振り分けるトークンは自由度の高いsymbol_typeという型の変数に振り分けられる、ということです。
 scanner.lに戻ります。
id    [a-zA-Z][a-zA-Z_0-9]*
int   [0-9]+
blank [ \t\r]
 というのは、id、int、blank正規表現で定義しています。例えばid1文字目はアルファベットで、2文字以降は数字もOKといった、興味深い正規表現ですね。
%{
  // Code run each time a pattern is matched.
  # define YY_USER_ACTION  loc.columns (yyleng);
%}
 はマクロでYY_USER_ACTIONloc.columns (yyleng);に置き換えるよう定義しています。
 続いてはparserファイルのように%%で囲まれたブロックになります
%%
%{
  // A handy shortcut to the location held by the driver.
  yy::location& loc = drv.location;
  // Code run each time yylex is called.
  loc.step ();
%}
{blank}+   loc.step ();
\n+        loc.lines (yyleng); loc.step ();
"-"        return yy::parser::make_MINUS  (loc);
"+"        return yy::parser::make_PLUS   (loc);
"*"        return yy::parser::make_STAR   (loc);
"/"        return yy::parser::make_SLASH  (loc);
"("        return yy::parser::make_LPAREN (loc);
")"        return yy::parser::make_RPAREN (loc);
":="       return yy::parser::make_ASSIGN (loc);

{int}      return make_NUMBER (yytext, loc);
{id}       return yy::parser::make_IDENTIFIER (yytext, loc);
.          {
             throw yy::parser::syntax_error
               (loc, "invalid character: " + std::string(yytext));
}
<<EOF>>    return yy::parser::make_YYEOF (loc);
%%
 まずflexlocationクラスstep()という関数呼び出しで、解析を進めるのが分かります。
 そのあとの{blank}+は、上で定義した正規表現blankの再利用です。ですから
{blank}+   loc.step ();
 は
[ \t\r]+   loc.step ();
 と等価です。
 改行は
\n+        loc.lines (yyleng); loc.step ();
 とステップを進めます。loc.lines (yyleng);が行を数えています。
"-"        return yy::parser::make_MINUS  (loc);
"+"        return yy::parser::make_PLUS   (loc);
"*"        return yy::parser::make_STAR   (loc);
"/"        return yy::parser::make_SLASH  (loc);
...省略
 はflexの定義が続きますが、それぞれyy::parser::make_なんたらという関数を呼んでいます。それぞれyy::parser::make_NUMBERと同様parser.hhに生成される関数です。このmake_なんたらは重要で、なんたらBisonのyyファイル
%token
  ASSIGN  ":="
  MINUS   "-"
  PLUS    "+"
  STAR    "*"
  SLASH   "/"
  LPAREN  "("
  RPAREN  ")"
;
 のように再利用されます。
.          {
             throw yy::parser::syntax_error
               (loc, "invalid character: " + std::string(yytext));
}
 は、それぞれのトークンに合致しないものはエラーとする処理です。std::string(yytext)で、エラー原因を表示してます。
<<EOF>>    return yy::parser::make_YYEOF (loc);
 は、YYEOFを作成しています。これはファイルの終端を表します。
 これはcalcCpp,ccにおいて
  for (int i = 1; i < argc; ++i)
    if (argv[i] == std::string ("-p"))
      drv.trace_parsing = true;
    else if (argv[i] == std::string ("-s"))
      drv.trace_scanning = true;
    else if (!drv.parse (argv[i]))
      std::cout << drv.result << '\n';
    else
      res = 1;
 の!drv.parseが終了したときの処理へとつながります。すなわちdrv.resultを出力すると最後の計算結果が表示されます。
 drv.resultに結果を入れているのはどこかというと、parser.yyunitブロックで
unit: assignments exp  { drv.result = $2; };
 と代入しています。それがtest.txtにおける
three := 3
seven := one + two * three
seven * seven

 のseven * sevenの表示につながるわけです。
 以上でscanner.l%%による字句解析の終了なります。
 そのあとの関数の実体はmake_NUMBERdriver.hhで宣言された関数2つ(driver::scan_begin ()、driver::scan_end ())となります。