独自のコンパイラを作成する: コマンド ライン モジュールを実装する

これまでの一連の章では、字句解析のためのさまざまなアルゴリズムを完成させました。正規表現文字列の解析、NFA 状態の構築、NFA から DFA 状態マシンへの変換、そして最後に状態マシンの最小化が含まれます。次に、字句解析モジュールのエンジニアリング実装に焦点を当てます。つまり、すべてのアルゴリズムを組み合わせて、利用可能なしたがって、次の章では、コンパイル原理のアルゴリズムではなく、エンジニアリングによる実装に焦点を当てます。

コンパイルの原則とアルゴリズムを強調する私たちのコラムが、エンジニアリングの実装に多大な労力を費やしているのはなぜでしょうか。英語には「you do not know it if you can't build it」という諺がありますが、つまり、それができないということは、それを習得していないということです。これが私たちの伝統的な教育の弱点です。コンパイル原理、オペレーティング システムなどのコンピューター コースを受講している場合、多くの名詞やアルゴリズムの説明を習得していますが、これらのコースを完了して試験に合格した後は、この知識を習得したことになりますか? オペレーティング システムを学習しても実行可能なシステムを作成できず、コンパイルの原理を学習してもコードをコンパイルできるコンパイラーを作成できない場合、それは自分が持っている知識を実際に把握していないことを意味します。学んだけど、漠然としているだけです。半分理解しています。

それを本当に習得するには、機能する具体的な実体を構築する必要があります。この特定のエンティティを実現する過程で、理解していると思っていた多くのアルゴリズムや概念が、実際にはまったく習得されていないことがわかります。このセクションの初めでは、GoLex にさらに複雑な関数を追加します。GoLex ツールが完成すると、その関数は次のようになります: GoLex プログラムの実行中に、input.lex と lex という 2 つのファイルを入力する必要があります
画像の説明を追加してください
。 .par のうち、input.lex は lex.par が実際には C 言語のテンプレート ファイルであることはすでに知っています。次の章では、その内容の分析と実装に多くの労力を費やします。GoLex はこれらの内容を読み取ります2 つのファイルを作成し、lex .yy.c と lex.yy.h という 2 つのファイルを生成すると、これら 2 つのファイルは特定の言語の字句パーサーのコードになります。SQL の語彙を認識できるプログラムを開発するとします。次に、SQL 言語のキーワードと変数名を特定します。文字列に対応する正規表現が input.lex に配置されるのを待ってから、GoLex を呼び出して 2 つの C 言語ソース コード ファイル lex.yy.c および lex を生成します。 .yy.h を実行し、gcc を使用してこれらのファイルをコンパイルし、最後に実行可能ファイル a.out を取得します。実行可能ファイル a.out は、SQL コード ファイルの字句解析を実行するために使用できる実行可能ファイルです。つまり、GoLex は実際に使用されるプログラムです別の実行可能プログラムのソース コードを生成するため、これは微積分の 2 番目のステップに似ています。

ナンセンスな話はやめて、できれば押し付けないでください。まず、プロジェクト ディレクトリに cmd という名前のフォルダーを作成し、次に cmd.go という名前のファイルを作成します。実装コードは次のとおりです。

package command_line

import (
	"fmt"
	"time"
)

type CommandLine struct {
    
    
}

func NewCommandLine() *CommandLine {
    
    
	return &CommandLine{
    
    }
}

func (c *CommandLine) Signon() {
    
    
	//这里设置当前时间
	date := time.Now()
	//这里设置你的名字
	name := "yichen"
	fmt.Printf("GoLex 1.0 [%s] . (c) %s, All rights reserved\n", date.Format("01-02-2006"), name)
}

上記のコードを実行すると、「著作権」に関する情報が一行出力されるので、何かすごいことをやったような気分になり、「自分は偉大な達人だ」という達成感を得ることができます。以下では、非圧縮 DFA に C 言語のコメントを出力するために使用される PrintHeader という関数を提供します。まず、元々 main 関数にあったコードを CommandLine オブジェクトのコンストラクターに移動します。関連するコードは次のとおりです。

package command_line

import (
	"fmt"
	"nfa"
	"time"
)

type CommandLine struct {
    
    
	lexerReader  *nfa.LexReader
	parser       *nfa.RegParser
	nfaConverter *nfa.NfaDfaConverter
}

func NewCommandLine() *CommandLine {
    
    
	lexReader, _ := nfa.NewLexReader("input.lex", "output.py")
	lexReader.Head()
	parser, _ := nfa.NewRegParser(lexReader)
	start := parser.Parse()
	nfaConverter := nfa.NewNfaDfaConverter()
	nfaConverter.MakeDTran(start)
	nfaConverter.PrintDfaTransition()

	return &CommandLine{
    
    
		lexerReader:  lexReader,
		parser:       parser,
		nfaConverter: nfaConverter,
	}
}

func (c *CommandLine) PrintHeader() {
    
    
	//针对未压缩的 DFA 状态就,输出对应的 c 语言注释
	c.nfaConverter.PrintUnCompressedDFA()
	//打印基于 c 语言的跳转表
	c.nfaConverter.PrintDriver()
}

func (c *CommandLine) Signon() {
    
    
	//这里设置当前时间
	date := time.Now()
	//这里设置你的名字
	name := "yichen"
	fmt.Printf("GoLex 1.0 [%s] . (c) %s, All rights reserved\n", date.Format("01-02-2006"), name)
}


次に、ファイル nfa_to_dfa を入力し、上記で呼び出された 2 つの関数をクラス NfaDfaConverter に追加します。実装は次のとおりです。

func (n *NfaDfaConverter) PrintUnCompressedDFA() {
    
    
	fmt.Fprint(n.fp, "ifdef __NEVER__\n")
	fmt.Fprint(n.fp, "/*------------------------------------------------\n")
	fmt.Fprint(n.fp, "DFA (start state is 0) is :\n *\n")
	nrows := n.nstates
	charsPrinted := 0
	for i := 0; i < nrows; i++ {
    
    
		dstate := n.dstates[i]
		if dstate.isAccepted == false {
    
    
			fmt.Fprintf(n.fp, "* State %d [nonaccepting]", dstate.state)
		} else {
    
    
			//这里需要输出行数
			//fmt.Fprintf(n.fp, "* State %d [accepting, line %d <", i, )
			fmt.Fprintf(n.fp, "* State %d [accepting, line %d, <%s>]\n", i, dstate.LineNo, dstate.acceptString)
			if dstate.anchor != NONE {
    
    
				start := ""
				end := ""
				if (dstate.anchor & START) != NONE {
    
    
					start = "start"
				}
				if (dstate.anchor & END) != NONE {
    
    
					end = "end"
				}
				fmt.Fprintf(n.fp, " Anchor: %s %s", start, end)
			}
		}
		lastTransition := -1
		for j := 0; j < MAX_CHARS; j++ {
    
    
			if n.dtrans[i][j] != F {
    
    
				if n.dtrans[i][j] != lastTransition {
    
    
					fmt.Fprintf(n.fp, "\n * goto %d on ", n.dtrans[i][j])
					charsPrinted = 0
				}
				fmt.Fprintf(n.fp, "%s", n.BinToAscii(j))
				charsPrinted += len(n.BinToAscii(j))
				if charsPrinted > 56 {
    
    
					//16 个空格
					fmt.Fprintf(n.fp, "\n *                ")
					charsPrinted = 0
				}
				lastTransition = n.dtrans[i][j]
			}
		}
		fmt.Fprintf(n.fp, "\n")
	}
	fmt.Fprintf(n.fp, "*/ \n\n")
	fmt.Fprintf(n.fp, "#endif\n")
}

func (n *NfaDfaConverter) PrintDriver() {
    
    
	text := "输出基于 DFA 的跳转表,首先我们将生成一个 Yyaccept数组,如果 Yyaccept[i]取值为 0," +
		"\n\t那表示节点 i 不是接收态,如果它的值不是 0,那么节点是接受态,此时他的值对应以下几种情况:" +
		"\n\t1 表示节点对应的正则表达式需要开头匹配,也就是正则表达式以符号^开始," +
		"2 表示正则表达式需要\n\t末尾匹配,也就是表达式以符号$结尾,3 表示同时开头和结尾匹配,4 表示不需要开头或结尾匹配"
	comments := make([]string, 0)
	comments = append(comments, text)
	n.comment(comments)
	//YYPRIVATE YY_TTYPE 是 c 语言代码中的宏定义,我们将在后面代码提供其定义
	//YYPRIVATE 对应 static, YY_TTYPE 对应 unsigned char
	fmt.Fprintf(n.fp, "YYPRIATE YY_TTYPE Yyaccept[]=\n")
	fmt.Fprintf(n.fp, "{\n")
	for i := 0; i < n.nstates; i++ {
    
    
		if n.dstates[i].isAccepted == false {
    
    
			//如果节点i 不是接收态,Yyaccept[i] = 0
			fmt.Fprintf(n.fp, "\t0  ")
		} else {
    
    
			anchor := 4
			if n.dstates[i].anchor != NONE {
    
    
				anchor = int(n.dstates[i].anchor)
			}
			fmt.Fprintf(n.fp, "\t%-3d", anchor)
		}

		if i == n.nstates-1 {
    
    
			fmt.Fprint(n.fp, "   ")
		} else {
    
    
			fmt.Fprint(n.fp, ",  ")
		}
		fmt.Fprintf(n.fp, "/*State %-3d*/\n", i)
	}
	fmt.Fprintf(n.fp, "};\n\n")
	//接下来的部分要在实现函数 DoFile 之后才好实现
	//TODO
}

ここでは、PrintDriver の一部しか実装していないことに注意してください。残りの部分については、次の章の C 言語コード テンプレートを実装する必要があります。その後、上記の TODO 部分を実装できます。ただし、上記のコードが完成した後、すでに lex.yy.c ファイルが表示されているので、main.go に次のコードを入力します。

package main

import (
	"command_line"
)

func main() {
    
    
	
	cmd := command_line.NewCommandLine()
	cmd.PrintHeader()
}

上記のコードを完成させて実行すると、次の内容を含む lex.yy.c ファイルが取得されます。

ifdef __NEVER__
/*------------------------------------------------
DFA (start state is 0) is :
 *
* State 0 [nonaccepting]
 * goto 1 on .
 * goto 2 on 0123456789
* State 1 [nonaccepting]
 * goto 3 on 0123456789
* State 2 [nonaccepting]
 * goto 4 on .
 * goto 5 on 0123456789
* State 3 [accepting, line 6, <  {printf("%s is a float number", yytext); return FCON;}>]

* State 4 [accepting, line 6, <  {printf("%s is a float number", yytext); return FCON;}>]

 * goto 6 on 0123456789
* State 5 [nonaccepting]
 * goto 1 on .
 * goto 5 on 0123456789
* State 6 [accepting, line 6, <  {printf("%s is a float number", yytext); return FCON;}>]

 * goto 7 on 0123456789
* State 7 [accepting, line 6, <  {printf("%s is a float number", yytext); return FCON;}>]

 * goto 7 on 0123456789
*/ 

#endif

/*--------------------------------------
 * 输出基于 DFA 的跳转表,首先我们将生成一个 Yyaccept数组,如果 Yyaccept[i]取值为 0,
	那表示节点 i 不是接收态,如果它的值不是 0,那么节点是接受态,此时他的值对应以下几种情况:
	1 表示节点对应的正则表达式需要开头匹配,也就是正则表达式以符号^开始,2 表示正则表达式需要
	末尾匹配,也就是表达式以符号$结尾,3 表示同时开头和结尾匹配,4 表示不需要开头或结尾匹配
 */

YYPRIATE YY_TTYPE Yyaccept[]=
{
	0  ,  /*State 0  */
	0  ,  /*State 1  */
	0  ,  /*State 2  */
	4  ,  /*State 3  */
	4  ,  /*State 4  */
	0  ,  /*State 5  */
	4  ,  /*State 6  */
	4     /*State 7  */
};

出力されたC言語ファイルでは、まずジャンプテーブルの内容をコメントで出力し、次に受信ステータスの配列を出力していることがわかります。ノードiが受信状態であれば、配列Yyaccept[の対応する値が出力されます。 i] は 0. ではありません。それ以外の場合、対応する値は 0 です。次のセクションでは、C 言語テンプレート コードを詳しく学習し、このセクションのコードの TODO 部分を完成させます。詳細については、コーディングを検索してください。ディズニー オン ステーション B で、より詳細なデバッグ デモ ビデオを入手してください。

おすすめ

転載: blog.csdn.net/tyler_download/article/details/133347873