Как бы вы пошли на внедрение правила вне игры?
Я уже написал генератор, который делает трюк, но я хотел бы знать наилучший способ реализации правила вне игры.
Вкратце: Off-side rule означает в этом контексте, что отступ становится распознанным как синтаксический элемент.
Вот правило офсайда в псевдокоде для создания токенизаторов, которые фиксируют отступ в полезной форме, я не хочу ограничивать ответы языком:
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
Этот затвор не генерирует больше одного NEWLINE во времени и не генерирует NEWLINE, когда появляется отступ. Поэтому правила синтаксического анализа остаются довольно простыми. Это довольно хорошо, я думаю, но сообщите, есть ли лучший способ добиться этого.
При использовании этого в течение некоторого времени я заметил, что после DEDENTs может быть приятно испускать новую строку в любом случае, таким образом вы можете разделить выражения с NEWLINE, сохраняя INDENT DEDENT в качестве трейлера для выражения.
Ответы
Ответ 1
В последние пару лет я написал токенизаторы и парсеры для нескольких небольших языковых языков, ориентированных на углубления, и то, что у вас там, выглядит довольно разумным для меня, несмотря на то, что стоит. Если я не ошибаюсь, ваш метод очень похож на то, что делает Python, например, похоже, что он должен иметь некоторый вес.
Преобразование NEWLINE NEWLINE INDENT в просто INDENT перед тем, как он попадает в парсер, определенно кажется правильным способом делать что-то - это боль (IME), чтобы всегда смотреть на него в парсере! Я на самом деле сделал этот шаг как отдельный слой в том, что в итоге стало трехэтапным процессом: первый из них объединил то, что ваши lexer и layouter делают за вычетом всего материала NEWLINE (что сделало его очень простым), второе (также очень простое ) сложить сложенные последовательные NEWLINE и преобразовать NEWLINE INDENT в просто INDENT (или, фактически, COLON NEWLINE INDENT в INDENT, так как в этом случае всем отступом блоков всегда предшествовали двоеточия), тогда парсер был третьим этапом. Но мне также очень важно делать то, что вы описали, особенно если вы хотите отделить лексер от планировщика, который, предположительно, вы хотите сделать, если вы используете инструмент генерации кода например, сделать ваш лексер, как это обычно бывает.
У меня было одно приложение, которое должно было быть немного более гибким в отношении правил отступов, по существу оставляя синтаксический анализатор для принудительного применения их по мере необходимости. Следующие условия должны быть действительными в определенных контекстах, например:
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, поскольку вам нужно создать один INDENT для каждого столбца отступов и равное количество DEDENT на обратном пути, если вы не будете искать пути, чтобы выяснить, где уровни отступа будут заканчиваться, что не похоже на то, что вы хотите использовать токенизатор. В этом случае я попробовал несколько разных вещей и просто сохранил счетчик в каждом токене NEWLINE, который дал изменение в отступе (положительном или отрицательном) для следующей логической строки. (Каждый токен также сохранял все конечные пробелы, в случае необходимости его сохранения; для NEWLINE хранимые пробелы включали сам EOL, любые промежуточные пустые строки и отступ в следующей логической строке.) Никаких отдельных токенов INDENT или DEDENT вообще. Получение парсера для решения этого было немного больше работы, чем просто вложение INDENT и DEDENTs, и, возможно, это был ад с сложной грамматикой, которая нуждалась в генераторе воображаемых парсеров, но это было не так плохо, как я боялся, или. Опять же, нет необходимости в синтаксическом анализаторе смотреть в NEWLINE, чтобы увидеть, есть ли в этой схеме INDENT.
Тем не менее, я думаю, вы согласитесь, что разрешаете и сохраняете всевозможные сумасшедшие пробелы в токенизаторе /layouter и позволяете парсеру решать, что такое буквальный, а какой код - необычное требование! Вы, конечно, не хотели бы, чтобы ваш синтаксический анализатор был привязан к этому счетчику отступов, если вы просто хотели разобрать код Python, например. То, как вы делаете, почти наверняка подходит для вашего приложения и многих других. Хотя, если у кого-то еще есть мысли о том, как лучше всего это делать, я, очевидно, люблю их слышать....
Ответ 2
Недавно я экспериментировал с этим, и я пришел к выводу, что, по крайней мере, для моих нужд я хотел, чтобы NEWLINES отмечал конец каждого "утверждения", будь то последний оператор в отступом или нет, т.е. мне нужны новые строки еще до DEDENT.
Мое решение состояло в том, чтобы повернуть его на голову, и вместо NEWLINES, обозначающего конец строк, я использую токен LINE для отметки начала строки.
У меня есть лексер, который сбрасывает пустые строки (включая строки только для комментариев) и испускает один токен LINE с информацией об отступе последней строки. Затем моя функция предварительной обработки принимает этот токен и добавляет INDENT или DEDENT "между" любыми строками, в которых изменяется отступ. Так
line1
line2
line3
line4
даст токен потока
LINE "line1" INDENT LINE "line2" LINE "line3" DEDENT LINE "line4" EOF
Это позволяет мне писать четкие грамматические произведения для операторов, не заботясь о том, чтобы обнаружить конец операторов, даже если они заканчиваются вложенными, отступом, субблоками, что может быть затруднено, если вы вместо этого сопоставляете NEWLINES (и DEDENTS).
Вот ядро препроцессора, написанного в 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 в конце файла.
Ответ 3
Tokenizer в рубине для удовольствия:
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
Работает только для двухпозиционного отступа. Результат - это что-то вроде ["Hello there from level 0\n", :INDENT, "This\nis level\ntwo\n", :DEDENT, "This is level0 again\n"]
.