6

初步解释

我正在尝试使用自定义正则表达式引擎进行一些测试,但我厌倦了手动编写 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$"

标签允许 Show 和 Eq 实例,即使最终 NFA 可以包含循环。例如,对表达式建模

-- "^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 构建这个结构,我阅读了这个网站上关于递归构建类似列表的结构的条目,并提出了以下内容:

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]。

问题

当我进入下一步时,问题就来了:解析'|' for 或语句。这应该工作的方式是一个字符串,如:

-- "(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 构建“文字”链似乎也可以这种方式工作。

也许我应该使用 buildExpressionTable?这可能有助于解决这个特定问题,因为我可以将 ('$' <|> eof) 设为最高优先级。我开始尝试它,但我无法想象我将如何处理诸如星号加号和问号运算符之类的事情,因为它们都必须引用自己。

(编辑:我再次尝试使用 buildExpressionTable,在我看来,这对于我想要做的事情来说太简单了。它不能原生处理堆叠的后缀运算符 [例如“a?*”],以及我的制作计划"'$' <|> eof" 最高优先级也不起作用,因为它只会附加到最后一个解析的'term',而不是整个字符串。即使我可以做到,'$' 运算符也会是向后应用:它应该是最后一个解析的术语并馈送到前一个术语。我使用这个越多,我就越想知道我是否不应该在解析之前反转表达式字符串。)

问题

那么,我做错了什么?我确信有一种方法可以做我想做的事情,但到目前为止我还没有弄清楚。感谢您的时间。

脚注

[1] 如果您想确切了解我实际使用的内容,可以在此处找到。

[2] Open/CloseGroup 标签的想法是在 NFA 运行时跟踪组匹配。列出的表达式中的位置可能不完全正确,但是如果遇到 CloseGroup 标签,这种方式会正常工作,仅在找到相应的 OpenGroup 时才创建捕获组(即在上面的示例中,我们只会在至少一个时创建捕获'b' 被看到)。

所有其他标签构造都是正确的,我已经测试过这个 NFA 与预期的字符串匹配。

[3]这里描述了 Thompson 实现,我的移植版本可以在这里看到。这完美地构建了 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
4

2 回答 2

5

您可能不会得到很多答案,因为这是一个巨大的问题,需要阅读一篇大论文才能让任何人考虑写一个答案。

话虽如此,一个奇怪的巧合,我这周恰好试图从正则表达式构建 NFA。;-)


好的,所以最直接的问题是

Couldn't match expected type `x -> y` with actual type `Parser`.

在英语中,这意味着在某个地方你有一个函数而不是解析器。快速浏览一下您的代码表明您已经编写了

where
  p = p_regex <*> p_end 1

但是p_regex需要 1 个参数,而您没有提供一个参数。就是您的代码不进行类型检查的原因。


好的,退后一步,您的实际问题是什么?您想将正则表达式解析为 NFA,但论文希望您将正则表达式转换为后缀符号,然后解析它,然后构建 NFA?

看起来应该是可以的。当我实现这个时,我将解析和 NFA 生成作为单独的步骤,纯粹是为了检查解析器是否工作以及 NFA 生成是否单独工作。但这听起来应该是可能的。Parsec 允许您拥有用户状态,因此您可以将其用作堆栈来存储 NFA 片段。(或者如果您愿意,也可以明确地传递它。)

如果您想要一个更准确的答案,您可能需要将其缩减为一个更小、更集中的问题。

于 2013-05-12T10:10:10.167 回答
1

好的,所以问题基本上是:给定一个递归数据结构(在问题中定义),我如何创建一个解析器来一次性构建我的表达式。我最初的尝试本质上是一种“应用程序”。只要没有条件分支,我就能够建立递归结构。但是对于正则表达式解析,需要分支,这就是我的方法不适用于or语句的原因。

所以为了解决这个问题,我需要有一些状态。在函数式语言中携带状态的一种好方法是使用部分应用的函数。我已经有了一个基础,p_char上面的签名是:

p_char :: ExpParser (T.State Char -> T.State Char)

所以我需要将它们组合在一起的是组合多个(T.State Char -> T.State Char)函数的组合器。因此,有了这种洞察力,排序就变成了:

p_many1 :: ExpParser (T.State Char -> T.State Char) -> ExpParser (T.State Char -> T.State Char)
p_many1 p = do
    f <- p
    (p_many1 p >>= return . (f .)) <|> return f

现在对于or语句,我们需要的是接受像“a|b|c”这样的表达式并创建如下函数的东西:

\e -> Split (Literal 1 'a' e) (Split (Literal 2 'b' e) (Literal 3 'c' e))

所以要做到这一点,我们可以使用这个:

p_splitBy1 :: ExpParser (T.State Char -> T.State Char) -> Parsec String ParserState Char -> ExpParser (T.State Char -> T.State Char)
p_splitBy1 p sep = do
    f <- p
    (sep >> p_splitBy1 p sep >>= return . (\f' e -> Split (f e) (f' e))) <|> return f

这确实创造了我需要的结构。因此,如果将来其他人遇到类似的问题,也许这个问题/答案可以使用。

于 2013-05-16T19:33:50.003 回答