我正在尝试使用自定义正则表达式引擎进行一些测试,但我厌倦了手工编写NFA,所以我试图使解析器收效甚微.通常当人们解析正则表达式时,他们会创建多个中间结构,最终转换为最终机器.对于我简单的NFA定义,我相信解析实际上可以在一次传递中完成,但我还没有确定(a)为什么它实际上不能或(b)如何做,虽然我的解析器可以解析非常简单声明.
(简化)状态实体的定义如此[1]:
type Tag = Int data State a = Literal Tag a (State a) | Split (State a) (State a) | OpenGroup Tag (State a) | CloseGroup Tag (State a) | Accept -- end of expression like "abc" | Final -- end of expression like "abc$"
即使最终的NFA可以包含循环,标签也允许Show和Eq实例.例如,为表达式建模
-- "^a+(b*)c$"
我可以用[2]
c = Literal 3 'c' $Final 1 b = OpenGroup 1 $Literal 2 'b' bs bs = Split b $CloseGroup 1 c expr = Literal 1 'a' $Split expr bs
我通过将Thompson NFA实现的C实现移植到Haskell,为这个语法(没有组标签)创建了一个功能的基于堆栈机器的解析器,但是这需要两次传递[3]来构建,并且需要第三次传递到上述结构.
因此,为了通过Parsec构建这个结构,我阅读了这个网站上有关递归构建类似List的结构的条目,并提出了以下内容:
import Control.Applicative import Text.Parsec hiding (many,optional,(<|>)) import Text.ExpressionEngine.Types import qualified Text.ExpressionEngine.Types as T type ParserState = Int type ExpParser = Parsec String ParserState type ExpParserS a = ExpParser (T.State a) parseExpression :: String -> T.State Char parseExpression e = case runParser p 1 e e of Left err -> error $show err Right r -> r where p = p_rec_many p_char $p_end 1 p_rec_many :: ExpParser (T.State a -> T.State a) -> ExpParserS a -> ExpParserS a p_rec_many p e = many' where many' = p_some <|> e p_some = p <*> many' p_end :: Int -> ExpParserS a p_end n = (Final n <$char '$') <|> (Accept n <$eof) step_index :: ExpParser Int step_index = do index <- getState updateState succ return index p_char = do c <- noneOf "^.[$()|*+?{\\" i <- step_index return $Literal i c
这足以解析像“ab”和“abc $”这样的字符串[4].
问题
当我进入下一步时解决问题:解析’|’或声明.这应该的方式是,一个字符串,如:
-- "(a|b|c)$"
应该创建以下结构:
final = Final 1 c = Literal 3 'c' final b = Split (Literal 2 'b' final) c a = Split (Literal 1 'a' final) b
这意味着构建或语句的解析器必须采用它之后的替代表达式并将其传递给所有分支(我不认为更改Split以获取列表而是更改任何内容,因为每个条目仍然必须接收相同的以下表达).我的尝试是:
p_regex :: T.State Char -> ExpParser (T.State Char) p_regex e = do first <- p_rec_many p_char $pure e (Split first <$> (char '|' *> p_regex e)) <|> return first
主解析器更改为:
parseExpression :: String -> T.State Char parseExpression e = case runParser p 1 e e of Left err -> error $show err Right r -> r where p = p_regex <*> p_end 1
但这无法输入检查[5].我希望这是正确的,因为p_regex必须有一个构建的(状态a)对象,并且使用p_rec_many构建“Literal”链似乎也是这样工作的.
也许我应该使用buildExpressionTable?这可能有助于解决这一特定问题,因为我可以将(‘$’< |> eof)作为最高优先级.我开始尝试它,但我无法想象我将如何处理像星加,和问号运算符之类的东西,因为那些都必须引用自己.
(编辑:我再次尝试使用buildExpressionTable,在我看来,它对于我想要做的事情来说太简单了.它本身不能处理堆叠的后缀操作符[例如“a?*”],以及我的制作计划“”’< |> eof”最高优先级也不起作用,因为它只会附加到最后解析的’term’,而不是整个字符串.即使我可以这样做,’$’运算符将被反向应用:它应该是最后一个被解析的术语并被提供给前一个术语.我使用的越多,我就越想知道我是否应该在解析之前反转表达式字符串.)
题
那么,我做错了什么?我确信有办法做我想做的事情到目前为止我还没弄清楚.感谢您的时间.
脚注
[1]如果你想确切地看到我实际使用它,可以找到here.
[2] Open / CloseGroup标签的想法是在NFA运行时跟踪组匹配.列出的表达式中的位置可能不完全正确,但如果遇到CloseGroup标记仅在找到相应的OpenGroup时创建捕获组,则这种方式可以正常工作(即在上面的示例中,我们只会创建一个捕获,如果至少有一个’b’被看到了).
所有其他标记构造都是正确的,我已经测试过这个NFA符合预期的字符串.
[3] Thompson实现描述为here,我的端口可以看到here.这完全构建了NFA子集,但在结果结构中,每个下一个状态将被包装在Just中.这是因为我使用Nothing来表示悬空指针,后面的步骤将在正确的下一步中进行修补.我可以通过将所有(Just state)条目转换为(state)条目来将此结构转换为上面列出的结构,但这将是第三次传递.此实现已经需要第一遍将正则表达式转换为后缀表示法.
[4]导致
Literal 1 'a' (Literal 2 'b' (Accept 1))
和
Literal 1 'a' (Literal 2 'b' (Literal 3 'c' (Final 1)))
分别.
[5]
Couldn't match expected type `a0 -> b0' with actual type `ExpParser (T.State Char)' Expected type: T.State Char -> a0 -> b0 Actual type: T.State Char -> ExpParser (T.State Char) In the first argument of `(<*>)',namely `p_regex' In the expression: p_regex <*> p_end 1
话虽如此,巧合的是巧合,我本周碰巧正试图从正则表达式中建立NFA.