Sample Site

GAWK レキシカルアナライザをつくる

  レキシカルアナライザとは

レキシカルアナライザは日本語で字句解析器(プログラムです)といいます。使用目的はいろいろありますが、最大のものではコンピュータ言語そのものの構築や、他言語への翻訳プログラムのための初期段階処理として使われます。その主たる仕事は、プログラムソースに記述された字句の連なり(コード)を一塊の意味ある最少単位に分割し、リスト化(配列等に格納)することです。

例えば AWK ソースの場合、以下のコードは次表のように分割、リスト化します。

pos = match("r = @/\"prog\"/", /\/.+\//) #サンプル
No.文字列(トークン)属性型
No. 1pos識別子(変数名)
No. 2 空白(スペース)
No. 3=演算子(代入)
No. 4 空白(スペース)
No. 5match識別子(組込み関数名)
No. 6(句読記号(開きかっこ)
No. 7"r = @/\"prog\"/"文字列定数
No. 8,句読記号(カンマ)
No. 9 空白(スペース)
No.10/\/.+\//正規表現定数
No.11)句読記号(閉じかっこ)
No.12 空白(水平タブ)
No.13#サンプルコメント

識別子や演算子等に分割されたこのリストを、次のプロシージャ(構文解析等)または別の関数(検索抽出等)が利用しますので、レキシカルアナライザ単体で用をなすことはあまりありません。

構文解析/意味解析の前段としてのレキシカルアナライザにおいては、空白やコメント、その他特定の句読記号は不要となるため、字句解析で早々に失われる字句となります。筆者の目的はそれとは別ですので、今回作るレキシカルアナライザはそのまま、ありのままを残すモードを用意しました。

AWK 用レキシカルアナライザで字句解析を行う場合は No.7 の文字列定数、No.10 の正規表現定数、No.13 のコメント、これらが一文字ずつバラバラにならず、一塊のデータになっていることが特に肝要です。これらを正確に取り出せるのであれば、筆者の目的の半分は達成できたことになります。
もう半分は、属性型欄に記載してある字句の属性を、各データ(トークン)に紐付ける手法です。これには、GAWK4で実装された2次元配列(配列の配列)を使用することにしました。取り出したリストの使い勝手が、従来に比べ多少良くなると筆者は考えています。

筆者の目的とは、以下のメタなツールを作成することです。

ソースコード自体への加筆(コメンタ等)や整形(ビューティファイア等)、加工(html化等)、ソースの解析検査、他のソースコードからの抽出等、なんとなく内向的なツール作成ばかりではありますが、このレキシカルアナライザさえあれば、微妙に役立つものが作れそうな気がします。

蛇足ですが、そもそも、スクリプトファイルを読んで実行している時点で、当然ながら gawk.exe の中には、AWKソース専用の完璧なレキシカルアナライザが存在しています。GAWK のソース awkgram.y (構文解析器作成ツール Bison 用のファイル)の中に、yylex_ebcdic()/yylex()として実装されています。しかし、それをユーザーが AWK ソースの中で任意に使用することはできません。ゆえに自分で作るしかないのです、AWK で書かれた AWK スクリプトのためのレキシカルアナライザを。


awk_lex.awk

lex_awk() の仕組みを要約すると、

  • ・ループの中で、一文字(token の先頭)取り出す
  • ・その一文字を swtch case で属性のふるいにかけ、token を切り出す
  • ・その際、文字位置を示すカウンタは、切り出す token の次の一文字を指す
  • ・属性と token を配列に格納する

    です。

    正規表現定数と除算演算子の区別は直前の字句(空白除く)で判断しています。

    print/printf/delete はGAWKのステートメントですが、筆者の都合上、組み込み関数としています。

    「in」は厳密にいうと演算子ですが、ステートメントとしています。

    BEGIN
    1 : # awk_lex.awk:AWKスクリプト用レキシカルアナライザ 2 : 3 : #. BEGIN:初期化 4 : BEGIN { 5 : _lex_init(); 6 : _kyw_init(); 7 : lex_mode = 1; 8 : }
    ACTION_01
    9 : #. ACTION_01:結果をダンプ 10 : { 11 : lex_awk($0, arr, lex_mode); 12 : print "\nLine No. " NR "\n$0 :" $0; 13 : for (i in arr) print "属性 :" arr[i][0] " 字句 :" arr[i][1]; 14 : }
    END
    15 : #. END:エラーの報告 16 : END { 17 : if (_erct) 18 : for (i in _er) printf("\n\nNo.%2d: %s\n", i, _er[i]); 19 : }
    lex_awk()
    20 : #. lex_awk(): レキシカルアナライザ 21 : # 戻値: トークン数(resの要素数) ※空行 = 1 22 : # str:in: 1行のAWKソース 23 : # res:out: 2次元配列 res[x][0]=属性 res[x][1]=トークン(/"含む) 24 : # mode:in: 0 空白カンマ省略 1 原文のまま 2 1+param/local認識[s] 25 : # 行継続/次行にまたがるリテラル/正規表現に対応 26 : # [属性表] 27 : # a 先越的キーワード (kinds of Apriori -include,load,namespace) 28 : # k 文制御キーワード (keyword control statement) 29 : # f 組み込み関数 (function of built-in) 30 : # v 組み込み変数/定数 (variable/const-value of built-in) 31 : # u ユーザー定義関数 (user-defined function) 32 : # i 他の識別子 (identifier of variable) 33 : # n 数値 (numeric) 34 : # o 演算子等 (operator) 35 : # p カンマや括弧 (punctuation) 36 : # l 文字列リテラル (literal) 37 : # r 正規表現 (regular expression) 38 : # c コメント (comment) 39 : # w 空白 (white space -spaces and tabs) 40 : # s 空白 mode0 mode2 (separator of parameters and local var) 41 : # e 改行のみ (empty) 42 : function lex_awk(str, res, mode, len,\ 43 : once, fcon, i, ib, ch, prev,\ 44 : pos, ct, tmp, ret, fignore) { 45 : ct = i = 1; 46 : prev = "first"; #行頭 直前句初期化 47 : delete res; #配列初期化 48 : (!mode && !_cont_ch) ? fignore = 1 : fignore = 0; 49 : if (fignore) { #前後の空白除去 50 : sub(/^[ \t]+/, "", str); 51 : sub(/[ \t]+$/, "", str); 52 : } 53 : len = length(str); 54 : if (!len) { res[1][0] = "e"; res[1][1] = ""; return 1; } #空行 55 : if (substr(str, len) ~ /\\/) fcon = 1; #継続フラグ 56 : 57 : while (i <= len) { 58 : ch = substr(str, i, 1); #一文字切り出す 59 : ib = i; #ib 切り出し開始位置 60 : 61 : #mode0 空白と不要な記号を削除(非継続行) 62 : if (fignore) { 63 : if (ch ~ /[,\t]/) { i++; continue; } 64 : else if (ch ~ /#/ && prev ~ /first/) { 65 : res[1][0] = "e"; 66 : res[1][1] = ""; 67 : return 1; 68 : } 69 : else if (ch ~ / /) { #スペース 70 : if (!_inparam) { i++; continue; } #関数パラメータでない 71 : else { #param/local var 境界 72 : while (tmp = substr(str, ++i, 1)) 73 : if (tmp !~ / /) break; 74 : if (i - ib < 3) continue; 75 : else { #3個以上 明示的境界 76 : res[ct][0] = "s"; 77 : res[ct][1] = " "; 78 : ct++; 79 : continue; 80 : } 81 : } 82 : } 83 : } 84 : 85 : #複数行にまたがる正規表現/リテラル 前行引き継ぎ 86 : if (!once) #1度だけ実行 87 : if (!_cont_ch) once = 1; 88 : else { once = 1; ch = _cont_ch; } #["/] 89 : 90 : if (ch in _a_z) ch = "z"; #/[a-zA-Z_\$]/の代替 91 : 92 : switch (ch) { 93 : case /\$/: #フィールド変数(組み込み) 94 : while (tmp = substr(str, ++i, 1)) 95 : if (tmp !~ /[0-9]/) break; #$0,$1等はバラさない 96 : ret = prev = substr(str, ib, i - ib); 97 : res[ct][0] = "v"; 98 : break; 99 : case /z/: #識別子 100 : while (tmp = substr(str, ++i, 1)) #次の文字から(++i) 101 : if (!(tmp in _a_9)) break; #/[a_zA_Z0-9_]/の代替 102 : ret = prev = substr(str, ib, i - ib); 103 : 104 : if (ret in _kywd) { #文制御 105 : res[ct][0] = "k"; 106 : if (ret ~ /func|function/) _infunc = 1; 107 : } 108 : else if (ret in _bltf) res[ct][0] = "f"; #組み込み関数 109 : else if (ret in _bltv) res[ct][0] = "v"; #組み込み変数 110 : else if (ret in _prpd) res[ct][0] = "a"; #先越的キーワード 111 : else if (tmp ~ /\(/) res[ct][0] = "u"; #ユーザー定義関数 112 : else res[ct][0] = "i"; #変数 113 : break; 114 : case /[0-9]/: #数値 16/10/8進数/指数表記 (+-なし) 115 : if (ch ~ /0/ && substr(str, i + 1, 1) ~ /[xX]/) { 116 : i++; #x Hex 117 : while (tmp = substr(str, ++i, 1)) 118 : if (tmp ~ /[^0-9a-fA-F]/) break; 119 : } 120 : else { 121 : while (tmp = substr(str, ++i, 1)) 122 : if (tmp ~ /[^0-9\.]/) break; 123 : if (tmp !~ /[eE]/) ; #nop Oct/Dec 124 : else { #指数 125 : if (substr(str, ++i, 1) ~ /[\-+]/) i++; # i +1 or +2 126 : for (; i <= len; i++) # i=i init 127 : if (substr(str, i, 1) ~ /[^0-9]/) break; 128 : } 129 : } 130 : ret = prev = substr(str, ib, i - ib); 131 : res[ct][0] = "n"; 132 : break; 133 : case /"/: #文字列リテラル 134 : pos = litendpos(str, i); 135 : if (pos) { i = ++pos; _cont_ch = ""; } #両端の””含む 136 : else if (fcon) { i = len; _cont_ch = "\""; } #継続 137 : else if (_cont_ch ~ /"/ && str ~ /^"/) { i = 2; _cont_ch = ""; } 138 : else { err(NR, i); i++; _cont_ch = ""; } #エラー 139 : ret = substr(str, ib, i - ib); 140 : prev = ch; 141 : res[ct][0] = "l"; 142 : break; 143 : case /\//: #正規表現または除算演算子 144 : if (prev in _prev) { #正規表現定数 145 : pos = regendpos(str, i); 146 : if (pos) { i = ++pos; _cont_ch = ""; } 147 : else if (fcon) { i = len; _cont_ch = "/"; } 148 : else if (_cont_ch ~ /\// && str ~ /^\//) { 149 : i = 2; 150 : _cont_ch = ""; 151 : } 152 : else { err(NR, i); i++; _cont_ch = ""; } 153 : ret = substr(str, ib, i - ib); 154 : prev = ch; 155 : res[ct][0] = "r"; 156 : } 157 : else { #除算演算子 158 : tmp = substr(str, ++i, 1); 159 : ret = prev = ch; 160 : res[ct][0] = "o"; 161 : if (tmp ~ /=/) { i++; ret = prev = "/="; } 162 : } 163 : break; 164 : case /#/: #コメント 165 : if (!mode) { i = len + 1; continue; } #mode 0 コメント無視 166 : else { 167 : i = len + 1; #ループ強制終了 168 : ret = substr(str, ib); #残余文字列全て 169 : res[ct][0] = "c"; 170 : break; 171 : } 172 : case /\(/: 173 : i++; 174 : ret = prev = ch; 175 : res[ct][0] = "p"; 176 : if (_infunc) _inparam = 1; 177 : break; 178 : case /\)/: 179 : i++; 180 : ret = prev = ch; 181 : res[ct][0] = "p"; 182 : if (_infunc) _inparam = _infunc = 0; 183 : break; 184 : case /\t/: #tab インデント等 185 : while (tmp = substr(str, ++i, 1)) #まとめる 186 : if (tmp !~ /\t/) break; 187 : ret = substr(str, ib, i - ib); 188 : res[ct][0] = "w"; 189 : break; 190 : case / /: #スペース 191 : while (tmp = substr(str, ++i, 1)) 192 : if (tmp !~ / /) break; 193 : ret = substr(str, ib, i - ib); 194 : res[ct][0] = "w"; 195 : if (mode == 2 && _inparam && i - ib > 2) { #param/local境界 196 : res[ct][0] = "s"; 197 : } 198 : break; 199 : default: #演算子 句読記号 200 : ret = prev = ch; 201 : if (ch in _op1) res[ct][0] = "o"; #演算子 202 : else res[ct][0] = "p"; #句読記号 203 : tmp = substr(str, i, 2); #2文字演算子かも 204 : i++; 205 : if (tmp in _op2) { 206 : ret = prev = tmp; #書き換え 207 : res[ct][0] = "o"; 208 : i++; 209 : } 210 : break; 211 : } 212 : res[ct][1] = ret; #トークン格納 213 : ct++; 214 : } 215 : return ct - 1; #切り出したトークン数 216 : }
    _lex_init()
    217 : #. _lex_init():直前字句(正規表現用) 演算子 辞書を初期化 218 : # 戻値: 219 : function _lex_init( ct, ar, i, prev, op1, op2, a_z) { 220 : prev = "(.,.~.!~.==.!=.&&.||.first.case.@.="; 221 : op1 = "+,-,*,/,^,%,!,~,=,<,>,?,|,@"; 222 : op2 = "++,--,+=,-=,*=,/=,%=,^=,!~,==,!=,<=,>=,&&,||,>>,<<,::"; 223 : a_z = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"; 224 : #高速化のためのハッシュテーブル 225 : ct = split(a_z, ar, ""); 226 : for (i = 1; i <= ct; i++) { _a_z[ar[i]]; _a_9[ar[i]]; } 227 : for (i = 0; i < 10; i++) _a_9[i]; 228 : 229 : ct = split(prev, ar, "."); 230 : for (i = 1; i <= ct; i++) _prev[ar[i]]; 231 : 232 : ct = split(op1, ar, ","); 233 : for (i = 1; i <= ct; i++) _op1[ar[i]]; 234 : 235 : ct = split(op2, ar, ","); 236 : for (i = 1; i <= ct; i++) _op2[ar[i]]; 237 : 238 : delete _er[0]; _erct = 0; #glb変数 エラー用 239 : _cont_ch = ""; #glb変数 リテラル/正規表現の次行持越し 240 : _infunc = 0; _inparam = 0; #glb変数 関数パラメータフラグ 241 : }
    _kyw_init()
    242 : #. _kyw_init():GAWKキーワード辞書 243 : # 戻値: 244 : function _kyw_init( ar, i, ct, p_str, k_str, f_str, v_str) { 245 : #先越的なもの 246 : p_str = "include,load,namespace"; 247 : #文制御 248 : k_str = "BEGIN,BEGINFILE,END,ENDFILE,next,exit,func,function,return,\ 249 : if,else,for,in,do,while,break,continue,switch,case,default,nextfile"; 250 : #組み込み関数 251 : f_str = "delete,index,patsplit,typeof,length,substr,match,split,sub,\ 252 : gsub,gensub,sprintf,strtonum,tolower,toupper,print,printf,getline,system,\ 253 : close,sin,cos,atan2,exp,log,int,sqrt,srand,rand,strftime,systime,mktime,\ 254 : and,or,xor,compl,lshift,rshift,fflush,dcgettext,dcngettext,\ 255 : bindtextdomain,asort,asorti,isarray"; 256 : #組み込み変数(フィールド変数除く) 257 : v_str = "FS,OFS,NF,RS,ORS,NR,FNR,ARGV,ARGC,ARGIND,FILENAME,ENVIRON,\ 258 : ERRNO,OFMT,CONVFMT,FIELDWIDTHS,IGNORECASE,RLENGTH,RSTART,SUBSEP,PROCINFO,\ 259 : TEXTDOMAIN,SYMTAB,FUNCTAB,RT,FPAT"; 260 : 261 : ct = split(p_str, ar, ","); 262 : for (i = 1; i <= ct; i++) _prpd[ar[i]]; 263 : 264 : ct = split(k_str, ar, ","); 265 : for (i = 1; i <= ct; i++) _kywd[ar[i]]; 266 : 267 : ct = split(f_str, ar, ","); 268 : for (i = 1; i <= ct; i++) _bltf[ar[i]]; 269 : 270 : ct = split(v_str, ar, ","); 271 : for (i = 1; i <= ct; i++) _bltv[ar[i]]; 272 : }
    litendpos()
    273 : #. litendpos():右ダブルクォートの正確な位置を返す(C/AWK) 274 : # 戻値: 右ダブルクォート位置 275 : # str:in: 文字列 276 : # pos:in: 左ダブルクォート位置 277 : function litendpos(str, pos, rpos, escape, ch) { 278 : rpos = escape = 0; 279 : while (ch = substr(str, ++pos, 1)) { #posの次文字から 280 : if (rpos) break; 281 : switch (ch) { 282 : case /\\/: 283 : (escape) ? escape = 0 : escape = 1; #偽 フラグ設定 284 : break; 285 : case /"/: 286 : (escape) ? escape = 0 : rpos = pos; #偽 終端 287 : break; 288 : default: 289 : (escape) ? escape = 0 : 0; #真 フラグ解除 290 : break; 291 : } 292 : } 293 : return rpos; 294 : }
    regendpos()
    295 : #. regendpos(): 正規表現末尾の正確な位置を返す(AWK) 296 : # 戻値: 正規表現終端位置(右スラッシュ位置) 297 : # str:in: 文字列 298 : # pos:in: 左スラッシュ位置 299 : function regendpos(str, pos, rpos, escape, bracket, ch) { 300 : rpos = escape = bracket = 0; 301 : while (ch = substr(str, ++pos, 1)) { #posの次文字から 302 : if (rpos) break; 303 : switch (ch) { 304 : case /\\/: 305 : (escape) ? escape = 0 : escape = 1; #偽 Eフラグ設定 306 : break; 307 : case /\[/: 308 : (escape) ? escape = 0 : bracket = 1; #偽 Bフラグ設定 [ 内 ] 309 : break; 310 : case /]/: 311 : (escape) ? escape = 0 : bracket = 0; #偽 Bフラグ解除 [ ]外 312 : break; 313 : case /\//: 314 : (escape || bracket) ? escape = 0 : rpos = pos; #両フラグ 0 315 : break; 316 : default: 317 : (escape) ? escape = 0 : 0; #真 Eフラグ解除 318 : break; 319 : } 320 : } 321 : return rpos; 322 : }
    err()
    323 : #. err(): リテラル/正規表現のエラー 324 : # 戻値: 325 : # line:in: エラー行 326 : # pos:in: エラー桁(先頭からn番目の文字)※TABを1文字として数えている 327 : function err(line, pos) { 328 : _er[++_erct] = "Error line: " line " position: " pos; 329 : }

    筆者が最もハマった箇所は、正規表現定数「regendpos」と文字列定数「litendpos」の切り出しです。フラグ操作で解決することに気がつくまで、ずいぶん時間を浪費しました。match()を使って正規表現をこねくり回しましたが、GAWKの正規表現では複雑なものを正確に切り出せませんでした。正規表現は便利なものですが、決して万能ではないと思い知った次第です。

    また、スクリプト作成中に、正規表現はマッチする文字候補が多くなるに連れ、実行速度は遅くなっていくという体験もできましたので、置換可能なものは連想配列(辞書)に置き換えて使用しています。

    レキシカルアナライザのテスト temp.awk

    注)まともなスクリプトではありません。

    1 : #. BEGIN; temp.awk 2 : @include "my_lib"; 3 : BEGIN { 4 : str = "The values indicate what gawk knows about the identifiers \ 5 : after it has \"finished parsing\" the program; \ 6 : they are not updated while the program runs. " 7 : 8 : reg1 = @/-?([1-9][0-9]*|0|([1-9][0-9]*\.|0\.)[0-9]+)|[abcdefghijk\ 9 : lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_]+/; 10 : if (reg2 ~ /\/[^\[\/\(\"\]\\]+\// || reg2 ~ /\/[^\(\)]+\//); 11 : else reg1 = @/[^\y]\/; #エラー 12 : 13 : ct = gindex(str, " ", a_pos); 14 : for (i = 1; i < ct + 1; i++) { 15 : printf("%3d, ", a_pos[i]); 16 : if (i % 5 == 0) print ""; 17 : } 18 : }

    実行結果  gawk421 -f awk_lex.awk temp.awk (mode = 1)

    ※水平タブを「→」に置換しています。

    コメント

    LineNo. : 1 $0 :#.→BEGIN; temp.awk 属性 :c 字句 :#.→BEGIN; temp.awk

    インクルード文

    LineNo. : 2 $0 :@include "my_lib"; 属性 :o 字句 :@ 属性 :a 字句 :include 属性 :w 字句 : 属性 :l 字句 :"my_lib" 属性 :p 字句 :;

    BEGINと最初のカーリーブレース

    LineNo. : 3 $0 :BEGIN { 属性 :k 字句 :BEGIN 属性 :w 字句 : 属性 :p 字句 :{

    文字列定数の改行と定数内のダブルクォート

    LineNo. : 4 $0 :→str = "The values indicate what gawk knows about the identifiers \ 属性 :w 字句 :→ 属性 :i 字句 :str 属性 :w 字句 : 属性 :o 字句 := 属性 :w 字句 : 属性 :l 字句 :"The values indicate what gawk knows about the identifiers 属性 :p 字句 :\ LineNo. : 5 $0 :after it has \"finished parsing\" the program; \ 属性 :l 字句 :after it has \"finished parsing\" the program; 属性 :p 字句 :\ LineNo. : 6 $0 :they are not updated while the program runs. "; 属性 :l 字句 :they are not updated while the program runs. " 属性 :p 字句 :;

    空行

    LineNo. : 7 $0 : 属性 :e 字句 :

    正規表現定数の改行

    LineNo. : 8 $0 :→reg1 = @/-?([1-9][0-9]*|0|([1-9][0-9]*\.|0\.)[0-9]+)|[abcdefghijk\ 属性 :w 字句 :→ 属性 :i 字句 :reg1 属性 :w 字句 : 属性 :o 字句 := 属性 :w 字句 : 属性 :o 字句 :@ 属性 :r 字句 :/-?([1-9][0-9]*|0|([1-9][0-9]*\.|0\.)[0-9]+)|[abcdefghijk 属性 :p 字句 :\ LineNo. : 9 $0 :lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_]+/; 属性 :r 字句 :lmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$_]+/ 属性 :p 字句 :;

    1行に複数の正規表現と正規表現内のエスケープスラッシュ

    LineNo. : 10 $0 :→if (reg2 ~ /\/[^\[\/\(\"\]\\]+\// || reg2 ~ /\/[^\(\)]+\//); 属性 :w 字句 :→ 属性 :k 字句 :if 属性 :w 字句 : 属性 :p 字句 :( 属性 :i 字句 :reg2 属性 :w 字句 : 属性 :o 字句 :~ 属性 :w 字句 : 属性 :r 字句 :/\/[^\[\/\(\"\]\\]+\// 属性 :w 字句 : 属性 :o 字句 :|| 属性 :w 字句 : 属性 :i 字句 :reg2 属性 :w 字句 : 属性 :o 字句 :~ 属性 :w 字句 : 属性 :r 字句 :/\/[^\(\)]+\// 属性 :p 字句 :) 属性 :p 字句 :;

    正規表現記述ミスによるエラーと部分コメント

    LineNo. : 11 $0 :→else reg1 = @/[^\y]\/;→#エラー 属性 :w 字句 :→ 属性 :k 字句 :else 属性 :w 字句 : 属性 :i 字句 :reg1 属性 :w 字句 : 属性 :o 字句 := 属性 :w 字句 : 属性 :o 字句 :@ 属性 :r 字句 :/ 属性 :p 字句 :[ 属性 :o 字句 :^ 属性 :p 字句 :\ 属性 :i 字句 :y 属性 :p 字句 :] 属性 :p 字句 :\ 属性 :o 字句 :/ 属性 :p 字句 :; 属性 :w 字句 :→ 属性 :c 字句 :#エラー

    エラー後の挙動(スルー)

    LineNo. : 12 $0 : 属性 :e 字句 :

    ユーザー定義関数

    AWKでは、ユーザー定義関数名と「(」の間に空白は入れられません。文字列連結のための空白が存在するため、文法的に許さないようです。一方、組み込み関数は空白OKとなりますが、コーディングの際には空白を入れないように統一した方が良いように思います。

    LineNo. : 13 $0 :→ct = gindex(str, " ", a_pos); 属性 :w 字句 :→ 属性 :i 字句 :ct 属性 :w 字句 : 属性 :o 字句 := 属性 :w 字句 : 属性 :u 字句 :gindex 属性 :p 字句 :( 属性 :i 字句 :str 属性 :p 字句 :, 属性 :w 字句 : 属性 :l 字句 :" " 属性 :p 字句 :, 属性 :w 字句 : 属性 :i 字句 :a_pos 属性 :p 字句 :) 属性 :p 字句 :;

    2文字演算子 インクリメント

    LineNo. : 14 $0 :→for (i = 1; i < ct + 1; i++) { 属性 :w 字句 :→ 属性 :k 字句 :for 属性 :w 字句 : 属性 :p 字句 :( 属性 :i 字句 :i 属性 :w 字句 : 属性 :o 字句 := 属性 :w 字句 : 属性 :n 字句 :1 属性 :p 字句 :; 属性 :w 字句 : 属性 :i 字句 :i 属性 :w 字句 : 属性 :o 字句 :< 属性 :w 字句 : 属性 :i 字句 :ct 属性 :w 字句 : 属性 :o 字句 :+ 属性 :w 字句 : 属性 :n 字句 :1 属性 :p 字句 :; 属性 :w 字句 : 属性 :i 字句 :i 属性 :o 字句 :++ 属性 :p 字句 :) 属性 :w 字句 : 属性 :p 字句 :{

    組み込み関数

    print/printf/delete は組み込み関数ではなく、AWKではステートメント扱いとなるのですが、属性の実態として if/while/for 等と同列に扱うには無理がありますので、組み込み関数属性としています。他言語の値を返さないメソッドと考えると割としっくりきます。

    LineNo. : 15 $0 :→→printf("%3d, ", a_pos[i]); 属性 :w 字句 :→→ 属性 :f 字句 :printf 属性 :p 字句 :( 属性 :l 字句 :"%3d, " 属性 :p 字句 :, 属性 :w 字句 : 属性 :i 字句 :a_pos 属性 :p 字句 :[ 属性 :i 字句 :i 属性 :p 字句 :] 属性 :p 字句 :) 属性 :p 字句 :;

    組み込み関数「()なし」

    LineNo. : 16 $0 :→→if (i % 5 == 0) print ""; 属性 :w 字句 :→→ 属性 :k 字句 :if 属性 :w 字句 : 属性 :p 字句 :( 属性 :i 字句 :i 属性 :w 字句 : 属性 :o 字句 :% 属性 :w 字句 : 属性 :n 字句 :5 属性 :w 字句 : 属性 :o 字句 :== 属性 :w 字句 : 属性 :n 字句 :0 属性 :p 字句 :) 属性 :w 字句 : 属性 :f 字句 :print 属性 :w 字句 : 属性 :l 字句 :"" 属性 :p 字句 :;

    記号のみの行と最後のカーリーブレース

    LineNo. : 17 $0 :→} 属性 :w 字句 :→ 属性 :p 字句 :} LineNo. : 18 $0 :} 属性 :p 字句 :}

    エラー表示 11行の15文字目の正規表現エラー

    No. 1: Error line: 11 position: 15

    レキシカルアナライザのテスト temp2.awk

    1 : function gindex(str, find, fpos, len, flen, i, ict) { 2 : delete fpos; 3 : len = length(str); 4 : flen = length(find); 5 : for (i = 1; i < len - flen + 2; i++) 6 : if (substr(str, i, flen) == find) fpos[++ict] = i; 7 : return ict; 8 : }

    実行結果  gawk421 -f awk_lex.awk temp2.awk (mode = 0)

    mode 0 時の動作ですが、特筆すべきは1行目の [fpos] の後の空白です。属性 s は引数としてのパラメータとローカル変数を分けるセパレータです。この空白を消すのはもったいないので、解析に役立てるよう残しました。

    Line No. 1 $0 :function gindex(str, find, fpos, len, flen, i, ict) { 属性 :k 字句 :function 属性 :u 字句 :gindex 属性 :p 字句 :( 属性 :i 字句 :str 属性 :i 字句 :find 属性 :i 字句 :fpos 属性 :s 字句 : 属性 :i 字句 :len 属性 :i 字句 :flen 属性 :i 字句 :i 属性 :i 字句 :ict 属性 :p 字句 :) 属性 :p 字句 :{ Line No. 2 $0 : delete fpos; 属性 :f 字句 :delete 属性 :i 字句 :fpos 属性 :p 字句 :; Line No. 3 $0 : len = length(str); 属性 :i 字句 :len 属性 :o 字句 := 属性 :f 字句 :length 属性 :p 字句 :( 属性 :i 字句 :str 属性 :p 字句 :) 属性 :p 字句 :; Line No. 4 $0 : flen = length(find); 属性 :i 字句 :flen 属性 :o 字句 := 属性 :f 字句 :length 属性 :p 字句 :( 属性 :i 字句 :find 属性 :p 字句 :) 属性 :p 字句 :; Line No. 5 $0 : for (i = 1; i < len - flen + 2; i++) 属性 :k 字句 :for 属性 :p 字句 :( 属性 :i 字句 :i 属性 :o 字句 := 属性 :n 字句 :1 属性 :p 字句 :; 属性 :i 字句 :i 属性 :o 字句 :< 属性 :i 字句 :len 属性 :o 字句 :- 属性 :i 字句 :flen 属性 :o 字句 :+ 属性 :n 字句 :2 属性 :p 字句 :; 属性 :i 字句 :i 属性 :o 字句 :++ 属性 :p 字句 :) Line No. 6 $0 : if (substr(str, i, flen) == find) fpos[++ict] = i; 属性 :k 字句 :if 属性 :p 字句 :( 属性 :f 字句 :substr 属性 :p 字句 :( 属性 :i 字句 :str 属性 :i 字句 :i 属性 :i 字句 :flen 属性 :p 字句 :) 属性 :o 字句 :== 属性 :i 字句 :find 属性 :p 字句 :) 属性 :i 字句 :fpos 属性 :p 字句 :[ 属性 :o 字句 :++ 属性 :i 字句 :ict 属性 :p 字句 :] 属性 :o 字句 := 属性 :i 字句 :i 属性 :p 字句 :; Line No. 7 $0 : return ict; 属性 :k 字句 :return 属性 :i 字句 :ict 属性 :p 字句 :; Line No. 8 $0 :} 属性 :p 字句 :}

    レキシカルアナライザのテスト temp3.awk

    1 : BEGIN { 2 : exp1=-1.9e-5-2.1e-5-0.9998+0.0002e4+0.0003e+4-4; #指数 3 : exp2=0x0f+0x01; #16進数 4 : exp3=010+010; #8進数 5 : print exp1,exp2,exp3; 6 : }

    実行結果  gawk421 -f awk_lex.awk temp3.awk (mode = 0)

    数値の字句解析です。通常の10進数、少数以外の指数、16進数、8進数を検証します。
    ソースは空白整形処理をしていない状態です。

    単項演算子(+-)は現在のところ普通の演算子として取り扱っています。数値にくっつけることも考えましたが、変数にそれを適用するわけにはいかないので、ちょっと扱いに困っています。

    Line No. 1 $0 :BEGIN { 属性 :k 字句 :BEGIN 属性 :p 字句 :{ Line No. 2 $0 :→exp1=-1.9e-5-2.1e-5-0.9998+0.0002e4+0.0003e+4-4;→#指数 属性 :i 字句 :exp1 属性 :o 字句 := 属性 :o 字句 :- 属性 :n 字句 :1.9e-5 属性 :o 字句 :- 属性 :n 字句 :2.1e-5 属性 :o 字句 :- 属性 :n 字句 :0.9998 属性 :o 字句 :+ 属性 :n 字句 :0.0002e4 属性 :o 字句 :+ 属性 :n 字句 :0.0003e+4 属性 :o 字句 :- 属性 :n 字句 :4 属性 :p 字句 :; Line No. 3 $0 :→exp2=0x0f+0x01;→#16進数 属性 :i 字句 :exp2 属性 :o 字句 := 属性 :n 字句 :0x0f 属性 :o 字句 :+ 属性 :n 字句 :0x01 属性 :p 字句 :; Line No. 4 $0 :→exp3=010+010;→#8進数 属性 :i 字句 :exp3 属性 :o 字句 := 属性 :n 字句 :010 属性 :o 字句 :+ 属性 :n 字句 :010 属性 :p 字句 :; Line No. 5 $0 :→print exp1,exp2,exp3; 属性 :f 字句 :print 属性 :i 字句 :exp1 属性 :i 字句 :exp2 属性 :i 字句 :exp3 属性 :p 字句 :; Line No. 6 $0 :} 属性 :p 字句 :}