オフサイドルールをどのように実装しますか?
質問
このトリックを実行するジェネレーターを既に作成しましたが、オフサイドルールを実装するための最良の方法を知りたいです。
短い:オフサイドルールは、このコンテキストではインデントが次のように認識されることを意味します。構文要素。
インデントを使用可能な形式でキャプチャするトークナイザーを作成するための擬似コードのオフサイドルールを次に示します。言語ごとに回答を制限したくありません:
token NEWLINE
matches r"\n\ *"
increase line count
pick up and store the indentation level
remember to also record the current level of parenthesis
procedure layout tokens
level = stack of indentation levels
push 0 to level
last_newline = none
per each token
if it is NEWLINE put it to last_newline and get next token
if last_newline contains something
extract new_level and parenthesis_count from last_newline
- if newline was inside parentheses, do nothing
- if new_level > level.top
push new_level to level
emit last_newline as INDENT token and clear last_newline
- if new_level == level.top
emit last_newline and clear last_newline
- otherwise
while new_level < level.top
pop from level
if new_level > level.top
freak out, indentation is broken.
emit last_newline as DEDENT token
clear last_newline
emit token
while level.top != 0
emit token as DEDENT token
pop from level
comments are ignored before they are getting into the layouter
layouter lies between a lexer and a parser
このレイアウト機能は、一度に複数の改行を生成することはなく、インデントが来るときに改行を生成することもありません。したがって、解析ルールは非常にシンプルなままです。かなり良いと思いますが、それを達成するより良い方法があればお知らせします。
これをしばらく使用している間、DEDENTの後に、とにかく改行を発行するのがいいかもしれないことに気づきました。この方法で、INDENT DEDENTを式のトレーラーとして保持しながら、NEWLINEで式を分離できます。
解決
過去数年間に、インデント中心のドメイン固有のいくつかの言語用のトークナイザーとパーサーを書いてきましたが、あなたが持っているものは、それが価値のあるものであるとしても、かなり合理的に見えます。誤解しない限り、あなたの方法は、例えばPythonが行うことと非常に似ており、ある程度の重みを持たせるべきだと思われます。
NEWLINE NEWLINE INDENTをパーサーにヒットする前に単にINDENTに変換することは、間違いなく正しい方法のように思えます-パーサーでそれを常に覗き込むのは苦痛です(IME)!私は実際にそのステップを別のレイヤーとして実行しましたが、最終的には3ステップのプロセスになりました:最初はレクサーとレイアウターが行うこととすべてのNEWLINE先読みのものを組み合わせたもの(非常にシンプルになりました)、2つ目(非常にシンプルです) )レイヤーは連続した改行を折り畳み、改行を単にINDENTに変換しました(または、実際にはCOLON NEWLINE INDENTからINDENT、この場合、すべてのインデントされたブロックは常にコロンに先行しているため)。その後、パーサーはその上の3番目のステージでした。しかし、特にレキサーとレイアウターを分離したい場合、おそらくコード生成ツールを使用している場合にしたい場合、あなたが説明した方法で物事を行うことは私にとって非常に理にかなっていますたとえば、一般的な慣習のようにレクサーを作成します。
インデントルールについてもう少し柔軟にする必要があるアプリケーションが1つあり、本質的にパーサーに必要なときにそれらを適用するようにしました。たとえば、以下は特定のコンテキストで有効である必要がありました。
this line introduces an indented block of literal text:
this line of the block is indented four spaces
but this line is only indented two spaces
これはINDENT / DEDENTトークンではひどく動作しません。インデントの各列に対して1つのINDENTを生成し、途中で同じ数のDEDENTを生成する必要があるためです。インデントレベルは最終的には存在することになりますが、これはトークナイザーにしたいとは思われません。その場合、いくつかの異なることを試して、次の論理行のインデントの変化(正または負)を与える各NEWLINEトークンにカウンターを格納するだけになりました。 (各トークンは、保存する必要がある場合に備えて、すべての末尾の空白も保存しました。NEWLINEの場合、保存された空白にはEOL自体、間にある空白行、および後続の論理行のインデントが含まれます。)個別のINDENTまたはDEDENTトークンはありません。パーサーにそれを処理させることは、単にINDENTとDEDENTをネストするよりも少し手間がかかり、派手なパーサージェネレーターを必要とする複雑な文法で地獄だったかもしれませんが、それは私が恐れていたほど悪くはありませんでした、どちらか。繰り返しますが、パーサーがこのスキームにインデントが来るかどうかを確認するためにNEWLINEを先読みする必要はありません。
それでも、トークナイザー/レイアウトでクレイジーに見えるホワイトスペースのすべての方法を許可および保持し、パーサーにリテラルとコードを決定させることは少し珍しい要件であることに同意すると思います!たとえば、Pythonコードを解析できるようにしたいだけなら、そのインデントカウンターでパーサーを操作したくないでしょう。あなたが物事をしている方法は、ほぼ間違いなくあなたのアプリケーションと他の多くの人にとって正しいアプローチです。この種のことをするのに最善の方法について他の誰かが考えているなら、私は明らかにそれらを聞きたいです...
他のヒント
私は最近これを実験してきましたが、少なくとも私のニーズのために、インデントされたブロックの最後のステートメントであるかどうかにかかわらず、各「ステートメント」の終わりをNEWLINESでマークしたかったという結論に達しましたつまり、DEDENTの前でも改行が必要です。
私の解決策は、その頭をオンにすることであり、改行で行の終わりを示す代わりに、LINEトークンを使用して行の始まりを示します。
空の行(コメントのみの行を含む)を折りたたみ、最後の行のインデントに関する情報を含む単一のLINEトークンを発行するレクサーがあります。次に、私の前処理関数はこのトークンストリームを取得し、「間に」INDENTまたはDEDENTを追加します。インデントが変更される行。だから
line1
line2
line3
line4
トークンストリームを提供します
LINE "line1" INDENT LINE "line2" LINE "line3" DEDENT LINE "line4" EOF
これにより、ステートメントがネストされたインデントされたサブブロックで終わる場合でも、ステートメントの終わりを検出することを心配せずに、ステートメントの明確な文法生成を書くことができます。 p>
O'Camlで記述されたプリプロセッサのコアは次のとおりです。
match next_token () with
LINE indentation ->
if indentation > !current_indentation then
(
Stack.push !current_indentation indentation_stack;
current_indentation := indentation;
INDENT
)
else if indentation < !current_indentation then
(
let prev = Stack.pop indentation_stack in
if indentation > prev then
(
current_indentation := indentation;
BAD_DEDENT
)
else
(
current_indentation := prev;
DEDENT
)
)
else (* indentation = !current_indentation *)
let token = remove_next_token () in
if next_token () = EOF then
remove_next_token ()
else
token
| _ ->
remove_next_token ()
括弧のサポートはまだ追加していませんが、これは単純な拡張機能です。ただし、ファイルの最後に迷っているLINEが出力されることはありません。
楽しみのためにルビーのトークナイザー:
def tokenize(input)
result, prev_indent, curr_indent, line = [""], 0, 0, ""
line_started = false
input.each_char do |char|
case char
when ' '
if line_started
# Content already started, add it.
line << char
else
# No content yet, just count.
curr_indent += 1
end
when "\n"
result.last << line + "\n"
curr_indent, line = 0, ""
line_started = false
else
# Check if we are at the first non-space character.
unless line_started
# Insert indent and dedent tokens if indentation changed.
if prev_indent > curr_indent
# 2 spaces dedentation
((prev_indent - curr_indent) / 2).times do
result << :DEDENT
end
result << ""
elsif prev_indent < curr_indent
result << :INDENT
result << ""
end
prev_indent = curr_indent
end
# Mark line as started and add char to line.
line_started = true; line << char
end
end
result
end
2スペースのインデントに対してのみ機能します。結果は [&quot; Hello there from level 0 \ n&quot ;,:INDENT、&quot; This \ nis level \ ntwo \ n&quot ;,:DEDENT、&quot; This is again0n \ n&quot;]
>。