确定一个标记是否在一行中的第一个

Determining whether a token is the first on a line

我正在为自定义编程语言编写词法分析器。首先,我想说这是一个个人练习,我想以 hand-written 的方式进行,而不是使用任何生成器工具,例如 Lex弹性.

我的语言中的一种语法是存在三种类型的注释:single-line、multi-line 和 doc:

  1. single-line 评论:
    code(x, y, z); %% a comment that ends at the end of the line
    moreCode(x, y, z);
    
  2. multi-line 评论:
    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)。现在我可以成功标记单个和 multi-line,并且我可以标记文档评论 就好像 它们是 multi-line。但这会导致一个问题:

不正确的行为

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

“文档评论”被视为 multi-line 评论。从上面可以看出,我的标记生成器错误地生成了这些标记:

  1. code — 标识符
  2. ( — 符号
  3. ) — 符号
  4. ; — 符号
  5. %%% This text is commented out. %%% — 评论
  6. notCommentedOut — 标识符
  7. ( — 符号
  8. ) — 符号
  9. ; — 符号

预期行为

上面的标记化是不正确的,因为我想强制 %%% 定界符在他们自己的行上以便注册为文档评论,并且我想要任何 %%% 而不是 在其自己的行中被视为 single-line 注释(因为它以 %% 开头)。 这意味着正确的标记化应该是:

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}
$$

我的方法

代码

(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。然后使用该标志通知下一个以 %%% 开头的标记。如果标志为真,则评论应在下一个 %%% 结束;否则它应该在下一个 \n 关闭。我不确定我是否喜欢这个选项,因为它涉及为每个代码标记设置标志。它也不考虑结束定界符,它也必须在自己的行上。

另一种选择是原路返回。当词法分析器到达以 %%% 开头的标记时,检查前一个标记以查看它是否为空白并包含 \n。如果是,则 %%% 标记是一个文档注释,应该在下一个 %%% 处关闭。如果不是,则为内联注释,应在 \n 处结束。我真的不喜欢这个选项,因为它涉及回溯,这会增加复杂性和时间。

这些选项是否正确?它们可行吗?推荐的?或者我应该采取另一种方法吗?

您在此处描述的两个选项似乎都很合理。我认为它们是构建词法分析器的另外两种通用技术的特例。

您使用布尔标志来存储您是否在一行的开头的想法是 扫描器状态 想法的一个特定实例。许多扫描仪由某种 finite-state 机器驱动,在某些情况下,这些 finite-state 机器的运行规则会根据上下文而变化。例如,扫描器在读取字符串文字时可能会切换模式以考虑转义序列、原始字符串文字等,使用与平常略有不同的规则集。或者,扫描器可能会跟踪一堆状态以支持嵌套注释。因此,从这个意义上说,您以这种方式处理事情是很好的伙伴。

您使用前导上下文的想法与 context-aware 属于同一个通用扫描仪系列。一些扫描仪会跟踪到目前为止已读取的信息,以确定如何解释新字符。例如,C++ 编译器在决定将 >> 解释为两个右尖括号还是单个移位运算符时,可能会跟踪它是否位于模板的中间。

至于两者中哪一个更容易 - 我怀疑使用全局位来跟踪扫描状态的想法可能更容易实现。如果您构建系统的想法是您可以将其推广以处理其他类型的扫描状态,那么这可能会相当优雅。

希望对您有所帮助!

对于这样的歧义,我用我的语言所做的是实现一些 "fake" 标记 types/tokenizer 状态。

所以当我遇到 % 我进入 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_PERCENTSTATE_TRIPLE_PERCENT 映射到 STATE_SINGLE_LINE_COMMENT)。

上下文:

我的分词器基本上是一个状态机。状态兼作令牌类型,我有一个 currentToken 变量,我向其中添加字符,并且当我使用字符时,它的状态会改变。我定期调用 endToken() 实际将 currentToken 的副本附加到令牌列表并将其重置为空。