Определение того, является ли токен первым в строке - PullRequest
1 голос
/ 24 июня 2019

Я нахожусь в процессе написания лексера для пользовательского языка программирования.Прежде всего, я хочу сказать, что это личное упражнение, и я хочу сделать это рукописным способом, а не использовать любые инструменты генератора, такие как Lex или Flex .

Один из синтаксисов в моем языке состоит в том, что есть три типа комментариев: однострочные, многострочные и документы:

  1. однострочные комментарии:
    code(x, y, z); %% a comment that ends at the end of the line
    moreCode(x, y, z);
    
  2. многострочные комментарии:
    code(x, %- a comment that starts and ends on the same line. -% y, z);
    moreCode(x, %- a comment that starts, contains a line break,
      and then ends. -% y, z);
    
  3. комментарии к документу:
    %%%
    a doc comment. the delimiters must be on their own line
    %%%
    code(x, y, z);
    

Проблема

Этот вопрос касается токенизации комментария типа документа(# 3).Прямо сейчас я могу успешно токенизировать одно- и многострочные, и я могу токенизировать комментарии к документам , как если бы они были многострочными.Но это приводит к проблеме:

Неправильное поведение

code(); %%% This text
is commented
out. %%% notCommentedOut();

«Комментарий к документу» рассматривается как многострочный комментарий.Исходя из вышесказанного, мой токенизатор неправильно создает следующие токены:

  1. code - идентификатор
  2. ( - символ
  3. ) - символ
  4. ; - символ
  5. %%% This text is commented out. %%% - комментарий
  6. notCommentedOut - идентификатор
  7. ( - символ
  8. )- символ
  9. ; - символ

Ожидаемое поведение

Указанный выше токенизация неверна, потому что я хочу включить разделители %%%свою собственную строку для регистрации в качестве комментария к документу, и я хочу, чтобы любые %%%, то есть , а не в отдельной строке, рассматривались как однострочный комментарий (потому что он начинается с %%).Это означает, что правильный токенизация должна быть:

code(); %%% This is commented out.
notCommentedOut();
'also' + !commentedOut; %%% also commented out
  1. code - идентификатор
  2. ( - символ
  3. ) - символ
  4. ; - символ
  5. %%% This is commented out. - комментарий
  6. notCommentedOut - идентификатор
  7. ( - символ
  8. ) - символ
  9. ; - символ
  10. 'also' - строка
  11. + - символ
  12. ! - символ
  13. commentedOut - идентификатор
  14. ; - символ
  15. %%% also commented out - комментарий

сходства

Другие языки имеют аналогичные конструкции, например, в Markdown, заголовках и изолированных блоках кода:

# this is a heading

foobar # this is not a heading

```
this is a fenced code block
```

foobar ``` this is not
a fenced code block ```

В LaTeX мы можем поместить уравнения блоков:

$$
f(x) = 2^{x + 1}
$$

My Approach

Код

(TypeScript и сокращен для ясности.)

// advance the scanner `n` number of characters
function advance(n: number = 1): void {
    if (n === 1) {
        // reassign c0 to the next character
        // reassign c1 to lookahead(1)
        // reassign c2 to lookahead(2)
    } else {
        advance(n - 1)
        advance()
    }
}
while (!character.done) {
    if (whitespace.includes(c0)) {
        const wstoken = new Token(character.value)
        wstoken.type = TokenType.WHITESPACE
        advance()
        while (!character.done && whitespace.includes(c0)) {
            wstoken.cargo += c0
            advance()
        }
        // yield wstoken // only if we want the lexer to return whitespace
        break;
    }

    const token = new Token(character.value)
    if (c0 === ENDMARK) {
        token.type = TokenType.EOF
        advance()
    } else if (c0 + c1 + c2 === comment_doc_start) { // we found a doc comment: `%%%`
        token.type = TokenType.COMMENT
        token.cargo += comment_doc_start
        advance(comment_doc_start.length)
        while (!character.done && c0 + c1 + c2 !== comment_doc_end) {
            if (c0 === ENDMARK) throw new Error("Found end of file before end of comment")
            token.cargo += c0
            advance()
        }
        // add comment_doc_end to token
        token.cargo += comment_doc_end
        advance(comment_doc_end.length)
    } else if (c0 + c1 === comment_multi_start) { // we found a multi-line comment: `%- -%`
        token.type = TokenType.COMMENT
        token.cargo += comment_multi_start
        advance(comment_multi_start.length)
        while (!character.done && c0 + c1 !== comment_multi_end) {
            if (c0 === ENDMARK) throw new Error("Found end of file before end of comment")
            token.cargo += c0
            advance()
        }
        // add comment_multi_end to token
        token.cargo += comment_multi_end
        advance(comment_multi_end.length)
    } else if (c0 + c1 === comment_line) { // we found a single-line comment: `%%`
        token.type = TokenType.COMMENT
        token.cargo += comment_line
        advance(comment_line.length)
        while (!character.done && c0 !== '\n') {
            if (c0 === ENDMARK) throw new Error("Found end of file before end of comment")
            token.cargo += c0
            advance()
        }
        // do not add '\n' to token
    } else {
        throw new Error(`I found a character or symbol that I do not recognize: ${c0}`)
    }
    yield token
}

Процесс мысли

Я думаю, что есть два варианта, оба из которых являютсяне желательно.

Один из вариантов - иметь глобальныйпеременная вне цикла while, логический флаг, указывающий, является ли предыдущий токен пробелом и содержит \n.Затем используйте этот флаг, чтобы сообщить следующий токен, который начинается с %%%.Если флаг имеет значение true, комментарий должен закрыться в следующем %%%;иначе это должно закрыться в следующем \n.Я не уверен, нравится ли мне эта опция, потому что она включает установку флага для каждого токена кода.Он также не учитывает конечный разделитель, который также должен находиться в отдельной строке.

Другой вариант - возврат.Когда лексер достигает токена, начинающегося с %%%, проверьте предыдущий токен, чтобы увидеть, является ли он пробелом и содержит \n.Если это так, токен %%% является комментарием к документу и должен закрыться на следующем %%%.Если нет, то это встроенный комментарий, который должен закрыться на \n.Мне действительно не нравится этот параметр, так как он включает в себя возврат, который увеличивает сложность и время.

Являются ли эти параметры даже удаленно правильными?Они выполнимы?Рекомендуемые?Или есть другой подход, который я должен использовать?

Ответы [ 2 ]

1 голос
/ 30 июня 2019

То, что я сделал на своем языке для такой неоднозначности, - это реализовать несколько «поддельных» типов токенов / состояний токенизатора.

Поэтому, когда я сталкиваюсь с %, я вхожу в STATE_PERCENT.

Если следующий символ - это - (%-), я перехожу на STATE_MULTILINE_COMMENT

Если следующий символ - % (%%...), я перехожу на STATE_DOUBLE_PERCENT.

Если следующий символ - это не что иное, как % (например, %%), я перехожу на STATE_SINGLE_LINE_COMMENT.

Но если это снова % (%%%...), я вхожу в STATE_TRIPLE_PERCENT.

Если следующий символ - разрыв строки, я перехожу к STATE_DOC_COMMENT, но если это что-то еще, я также перехожу к STATE_SINGLE_LINE_COMMENT.

Поскольку у меня есть центральная функция узкого места endToken(), которая просматривает текущее состояние, смещение и т. Д. И создает для этого структуру данных токена, я также могу посмотреть в этой функции, является ли состояние поддельным, и сопоставить его с соответствующее реальное состояние (например, STATE_DOUBLE_PERCENT и STATE_TRIPLE_PERCENT отображаются на STATE_SINGLE_LINE_COMMENT).

Контекст:

Мой токенизатор - это, по сути, конечный автомат. Состояние удваивается как тип токена, и у меня есть переменная currentToken, к которой я добавляю символы и чье состояние я меняю по мере потребления символов. Периодически я звоню endToken(), чтобы фактически добавить копию currentToken в список токенов и сбросить ее на пустой.

1 голос
/ 25 июня 2019

Обе опции, которые вы здесь описываете, кажутся разумными.Я думаю, что они являются частными случаями двух других общих методов построения лексических анализаторов.

Ваша идея использовать логический флаг для хранения информации о том, находитесь ли вы в начале строки, является частным случаем идеи состояния сканера .Многие сканеры управляются каким-то автоматом с конечным числом состояний, и в некоторых случаях правила, по которым эти машины с конечным состоянием работают, меняются в зависимости от контекста.Например, сканер может переключать режимы при чтении строкового литерала для учета escape-последовательностей, необработанных строковых литералов и т. Д., Используя несколько иной набор правил, чем обычно.В качестве альтернативы, сканер может отслеживать стек состояний для поддержки вложенных комментариев.Таким образом, в этом смысле вы находитесь в хорошей компании для того, чтобы подходить к таким вещам.

Ваша идея использовать ведущий контекст принадлежит к тому же общему семейству сканеров, которые учитывают контекст.Некоторые сканеры отслеживают информацию о том, что было прочитано до сих пор, чтобы определить, как интерпретировать новые символы.Например, компилятор C ++ может отслеживать, находится ли он в середине шаблона, при принятии решения о том, следует ли интерпретировать >> как две закрывающие угловые скобки или как один оператор сдвига битов.

Что касается того, какой из двухэто проще - я подозреваю, что идея использования глобального бита для отслеживания состояния сканирования может быть немного проще в реализации.Если вы строите систему с мыслью, что вы можете обобщить это для обработки других видов состояний сканирования, это, вероятно, будет довольно элегантно.

Надеюсь, это поможет!

...