レクサーがそのパーサーに返すトークンのデータ型は何ですか?


21

タイトルで述べたように、レクサーはどのデータ型をパーサーに返す/与える必要がありますか?ウィキペディアが持っている字句解析の記事を読んだとき、それは次のように述べました:

コンピュータサイエンスでは、字句解析とは、一連の文字(コンピュータプログラムやWebページなど)を一連のトークン(識別された「意味」を持つ文字列)に変換するプロセスです。

しかし、上記の声明と完全に矛盾して、別のサイトで質問した別の質問(好奇心があればコードレビュー)が回答されたとき、回答者は次のように述べました:

字句解析プログラムは通常、文字列を読み取り、これを語彙素のストリームに変換します。語彙素は数字のストリームである必要があります

そして彼はこの視覚を与えました:

nl_output => 256
output    => 257
<string>  => 258

後の記事で彼はFlex、既存のレクサーについて言及し、それを使って「ルール」を書くほうが、レクサーを手で書くよりも簡単だと言った。彼は私にこの例を与え始めました:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

私の洞察を深め、より多くの情報を得るために、WikipediaのFlexに関する記事を読みました。Flexの記事では、トークンを使用して、次の方法で一連の構文規則を定義できることが示されました。

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Flexレクサーはキーワードの文字列を返しているように思えます。ただし、特定の数値に等しい定数を返すこともできます。

レクサーが数値を返す場合、文字列リテラルはどのように読み取られますか?単一のキーワードでは数値を返すことは問題ありませんが、文字列をどのように処理しますか?字句解析プログラムは文字列を2進数に変換する必要はありません。その後、パーサーは数値を文字列に変換します。字句解析器が文字列を返し、パーサーに任意の数値文字列リテラルを実際の数値に変換させると、はるかに論理的(かつ簡単)に思えます。

または、レクサーは両方を返すことができますか?私はc ++で簡単な字句解析プログラムを作成しようとしました。これにより、関数の戻り値の型を1つだけにすることができます。したがって、私の質問をするように私を導く。

私の質問を段落に凝縮する:レキサーを書いて、それが1つのデータ型(文字列または数字)のみを返すことができると仮定すると、どちらがより論理的な選択でしょうか?


レクサーは、ユーザーが返すように指示したものを返します。設計で数値が必要な場合は、数値が返されます。明らかに、文字列リテラルを表現するには、それ以上が必要です。参照してください、それは解析の数値と文字列のレクサーの仕事ですか! 通常、文字列リテラルは「言語要素」とは見なされないことに注意してください。
ロバートハーベイ

@RobertHarveyでは、文字列リテラルを2進数に変換しますか?
クリスチャンディーン

私が理解しているように、レクサーの目的は、言語要素(キーワード、演算子など)を取得してトークンに変換することです。そのため、クォートされた文字列は言語要素ではないため、レクサーにとっては重要ではありません。レクサーを自分で書いたことはありませんが、引用符で囲まれた文字列は、そのまま(引用符も含めて)そのまま渡されると思います。
ロバートハーヴェイ

あなたの言っていることは、字句解析器は文字列リテラルを読んだり気にしたりしないということです。そして、パーサーはこれらの文字列リテラルを探す必要がありますか?これは非常に紛らわしいです。
クリスチャンディーン

回答:


10

一般に、字句解析および構文解析を通じて言語を処理している場合、字句トークンの定義があります。例えば:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

パーサーの文法があります:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

レクサーは入力ストリームを受け取り、トークンのストリームを生成します。トークンのストリームは、構文解析ツリーを生成するためにパーサーによって消費されます。場合によっては、トークンのタイプを知るだけで十分です(例:LPAREN、RBRACE、FOR)。しかし、場合によっては、トークンに関連付けられている実際のが必要になります。たとえば、IDトークンに遭遇した場合、後で参照しようとしている識別子を特定するときに、IDを構成する実際の文字が必要になります。

したがって、通常、次のようなものが多少あります。

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

そのため、レクサーがトークンを返すとき、そのタイプ(解析に必要)と、それが生成された文字のシーケンス(後で文字列と数値のリテラル、識別子、等。)。非常に単純な集約型を返すため、2つの値を返しているように感じるかもしれませんが、実際には両方の部分が必要です。結局のところ、次のプログラムを異なる方法で処理する必要があります。

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

これらは、トークンタイプの同じシーケンスを生成します:IF、LPAREN、NUMBER、GREATER_THAN、NUMBER、RPAREN、LBRACE、ID、LPAREN、STRING、RPAREN、SEMICOLON、RBRACE。つまり、それらも同じものを解析します。しかし、実際に解析ツリーで何かをしているときは、最初の数値の値が「2」(または「0」)であり、2番目の数値の値が「0」(または「2」 ')、および文字列の値が' 2> 0 '(または' 0> 2 ')であること。


私はあなたの言って何のほとんどを得るが、どのようにということをさString valueいっぱいを取得するつもり?文字列または数字で埋められますか?また、Stringタイプをどのように定義しますか?
クリスチャンディーン

1
@ Mr.Python最も単純なケースでは、字句生成に一致したのは単なる文字列です。したがって、foo(23、 "bar")が表示された場合、トークン[ID、 "foo"]、[LPAREN、 "("]、[NUMBER、 "23"]、[COMMA、 "、"が取得されます。 ]、[STRING、 "" 23 ""]、[RPAREN、 ")"]。その情報を保存することは重要です。または、別のアプローチを取り、値に文字列や数値などのユニオン型を持たせ、所有するトークンの種類に基づいて適切な値の種類を選択することもできます(たとえば、トークンの種類がNUMBERの場合) 、value.numを使用し、STRINGの場合はvalue.strを使用します。
ジョシュアテイラー

@MrPython 「また、文字列型をどのように定義しますか?」私はJavaっぽい考え方から書いていました。C ++で作業している場合は、C ++の文字列型を使用できます。Cで作業している場合は、char *を使用できます。ポイントは、トークンに関連付けられているポイント、対応する値、または値を生成するために解釈できるテキストを持っていることです。
ジョシュアテイラー

1
@ ollydbg23はオプションであり、不当なものではありませんが、システムの内部的な一貫性が低下します。たとえば、解析した最後の町の文字列値が必要な場合は、明示的にnull値を確認してから、トークンから文字列への逆ルックアップを使用して、文字列が何であるかを調べる必要があります。さらに、レクサーとパーサーの間のより緊密な結合です。LPARENが異なる文字列または複数の文字列に一致する可能性がある場合、更新するコードがさらにあります。
ジョシュアテイラー

2
@ ollydbg23 1つのケースは、単純な擬似縮小器です。簡単に実行できますparse(inputStream).forEach(token -> print(token.string); print(' '))(つまり、トークンの文字列値をスペースで区切って出力するだけです)。それは非常に簡単です。また、LPARENがメモリ内の定数文字列である可能性がある "("からのみ発生する可能性がある場合でも、トークンへの参照を含めることは、null参照を含めるよりもコストがかからない場合があります。私を特別なケースにしないコード
Joshua Taylor

6

タイトルで述べたように、レクサーはパーサーを返す/与えるデータ型はどれですか?

「トークン」、明らかに。レクサーはトークンのストリームを生成するため、トークンのストリームを返す必要があります。

彼は既存のレクサーであるFlexに言及し、それを使って「ルール」を書くほうが、レクサーを手で書くよりも簡単だと言いました。

機械生成のレクサーには、迅速に生成できるという利点があります。これは、字句文法が大きく変わると思われる場合に特に便利です。それらには、実装の選択に多くの柔軟性が得られないことが多いという欠点があります。

そうは言っても、誰がそれが「単純」であるかを気にしますか?通常、レクサーの作成は難しい部分ではありません!

字句解析プログラムを作成し、1つのデータ型(文字列または数値)のみを返すことができると仮定した場合、どちらがより論理的な選択でしょうか?

どちらでもない。字句解析器には通常、トークンを返す「次の」操作があるため、トークンを返す必要があります。トークンは文字列でも数字でもありません。それはトークンです。

私が書いた最後の字句解析器は「完全忠実」字句解析器でした。つまり、プログラム内のすべての空白とコメントの位置を追跡するトークンと、トークン内のコメントを返しました。私のレクサーでは、トークンは次のように定義されました:

  • 主要なトリビアの配列
  • トークンの種類
  • 文字単位のトークン幅
  • 末尾のトリビアの配列

トリビアは次のように定義されました:

  • トリビアの種類-空白、改行、コメントなど
  • 文字のトリビア幅

そのため、次のようなものがあった場合

    foo + /* comment */
/* another comment */ bar;

トークンの種類を持つ4つのトークンとしてlexのであろうとIdentifierPlusIdentifierSemicolon、および幅3、1、3、まず識別子トリビアからなる主要有するWhitespace4の幅とトリビア末尾Whitespace1の幅とPlus全く主要トリビアを持っていないのと1つの空白、コメント、および改行で構成される末尾のトリビア。最終的な識別子には、コメントとスペースなどの主要なトリビアがあります。

このスキームでは、ファイル内のすべての文字がレクサーの出力で考慮されます。これは、構文の色付けなどの便利なプロパティです。

もちろん、トリビアが必要ない場合は、種類と幅という2つのトークンを簡単に作成できます。

トークンとトリビアには幅だけが含まれており、ソースコード内の絶対位置は含まれていないことに気付くかもしれません。それは意図的です。このようなスキームには次の利点があります。

  • メモリとワイヤ形式でコンパクトです
  • 編集時の再字句化が可能です。これは、レクサーがIDE内で実行されている場合に役立ちます。つまり、トークンの編集を検出した場合、編集前にレクサーをいくつかのトークンにバックアップし、前のトークンストリームと同期するまでレキシングを再開します。文字を入力すると、その文字の後のすべてのトークンの位置が変更されますが、通常、1つまたは2つのトークンの幅が変更されるだけなので、その状態をすべて再利用できます。
  • すべてのトークンの正確な文字オフセットは、トークンストリームを反復処理し、現在のオフセットを追跡することで簡単に導出できます。正確な文字オフセットを取得したら、必要なときにテキストを簡単に抽出できます。

これらのシナリオを気にしない場合、トークンは種類と幅ではなく、種類とオフセットとして表すことができます。

しかし、ここで重要なことは、プログラミングは有用な抽象化を行う技術です。トークンを操作しているので、トークンよりも便利な抽象化を行うと、その下に実装の詳細を選択することができます。


3

一般に、トークンを表す番号(または使いやすい列挙値)とオプションの値(文字列、またはジェネリック/テンプレート値)を持つ小さな構造体を返します。別のアプローチは、追加のデータを運ぶ必要がある要素の派生型を返すことです。どちらも軽度の嫌悪感はありますが、実用的な問題に対する十分な解決策です。


軽度の不快感とはどういう意味ですか?文字列値を取得する非効率的な方法ですか?
クリスチャンディーン

@ Mr.Python-コードで使用する前に多くのチェックが行われますが、これは非効率的ですが、さらにコードが少し複雑/脆弱になります。
テラスティン

C ++でレクサーを設計するときに似たような質問があります。クラスの共有ポインタでToken *あるToken、またはを返すことができTokenPtrますToken。しかし、一部のレクサーがTokenTypeのみを返し、文字列または数値を他のグローバル変数または静的変数に格納することも確認しています。別の質問は、場所情報をどのように保存できるかということです。TokenType、String、およびLocationフィールドを持つToken構造体が必要ですか?ありがとう。
ollydbg23

@ ollydbg23-これらはどれでも機能します。構造体を使用します。また、非学習言語の場合は、パーサージェネレーターを使用します。
テラスティン

@Telastyn返信ありがとう。トークン構造体は、次のようなものになる可能性があるということstruct Token {TokenType id; std::string lexeme; int line; int column;}ですか?などのLexerのパブリック関数のPeekToken()場合、関数はToken *またはを返すことができTokenPtrます。私はしばらくの間、関数がTokenTypeを返すだけであれば、パーサーはトークンに関する他の情報をどのように取得しようとしますか?そのため、このような関数からの戻りには、データ型のようなポインターが推奨されます。私のアイデアについて何かコメントはありますか?ありがとう
-ollydbg23
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.