我正在为自定义编程语言编写词法分析器。首先,我想说这是个人练习,我想以手写方式完成,而不要使用任何生成器工具,例如 Lex 或 Flex 。< / p>
我的语言中的一种语法是注释的三种类型:单行,多行和文档:
code(x, y, z); %% a comment that ends at the end of the line
moreCode(x, y, z);
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);
%%%
a doc comment. the delimiters must be on their own line
%%%
code(x, y, z);
这个问题是关于标记注释的文档类型(#3)。现在,我可以成功地标记单行和多行,并且可以标记文档注释 就像它们是多行一样。但这会导致问题:
code(); %%% This text
is commented
out. %%% notCommentedOut();
“文档注释”被视为多行注释。综上所述,我的令牌生成器错误地生成了以下令牌:
code
—标识符(
-符号)
-符号;
-符号%%% This text
is commented
out. %%%
—评论notCommentedOut
—标识符(
-符号)
-符号;
-符号上面的标记化是不正确的,因为我想强制%%%
分隔符在自己的行上才能注册为文档注释,并且我希望任何{ em> not 在其单独的行上被视为单行注释(因为它以%%%
开头)。
这意味着正确的令牌化应该为:
%%
code(); %%% This is commented out.
notCommentedOut();
'also' + !commentedOut; %%% also commented out
—标识符code
-符号(
-符号)
-符号;
—评论%%% This is commented out.
—标识符notCommentedOut
-符号(
-符号)
-符号;
—字符串'also'
-符号+
-符号!
—标识符commentedOut
-符号;
—评论其他语言具有类似的构造,例如在Markdown,标题和受保护的代码块中:
%%% also commented out
在LaTeX中,我们可以放置块方程:
# this is a heading
foobar # this is not a heading
```
this is a fenced code block
```
foobar ``` this is not
a fenced code block ```
(为清晰起见,TypeScript进行了简化。)
$$
f(x) = 2^{x + 1}
$$
我想有两种选择,两者都不可取。
一个选择是在// 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
令牌是文档注释,应在下一个%%%
处关闭。如果不是,则为内嵌评论,应以%%%
结尾。我真的不喜欢此选项,因为它涉及回溯,这会增加复杂性和时间。
这些选项甚至可以远程纠正吗?他们可行吗?推荐的?还是我应该采取另一种方法?
答案 0 :(得分:1)
您在此处描述的两个选项似乎都是合理的。我认为它们是另外两种构建词法分析器的通用技术的特例。
您使用布尔标志存储您是否在行首的想法是扫描器状态想法的特定实例。许多扫描仪由某种有限状态机驱动,在某些情况下,这些有限状态机操作的规则会根据上下文而变化。例如,扫描程序在读取字符串文字时可能会使用与通常略有不同的规则集来切换模式,以说明转义序列,原始字符串文字等。或者,扫描程序可能会跟踪状态堆栈以支持嵌套注释。因此,从这个意义上讲,您是这样对待事情的好伙伴。
使用领先的上下文的想法与上下文感知的扫描仪相同。一些扫描仪会跟踪有关到目前为止所读内容的信息,以确定如何解释新字符。例如,当决定将>>
解释为两个闭合角括号还是单个位移运算符时,C ++编译器可能会跟踪其是否在模板中间。
关于两者中哪一个更容易-我怀疑使用全局位来跟踪扫描状态的想法可能更容易实现。如果您以可以将其通用化以处理其他种类的扫描状态的想法来构建系统,那可能会相当优雅。
希望这会有所帮助!
答案 1 :(得分:1)
我为使用这种歧义语所做的工作是实现一些“伪”令牌类型/令牌状态。
因此,当我遇到brew services reload nginx
时,我会进入brew doctor
。
如果下一个字符是%
(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
的副本实际附加到令牌列表中,并将其重置为空。