Использование Haskell Parsec для анализа регулярного выражения за один проход

объяснением

Я пытаюсь сделать некоторое тестирование с помощью пользовательского механизма регулярных выражений, но я устал писать NFAs вручную, поэтому я попытался сделать парсер с небольшим успехом. Обычно, когда люди анализируют регулярное выражение, они создают несколько промежуточных структур, которые в конечном итоге преобразуются в конечный компьютер. Для моего простого определения NFA я считаю, что синтаксический анализ может быть выполнен за один проход, хотя я еще не определил либо (а) почему он на самом деле не может или (Б) как это сделать, хотя мой парсер может анализировать очень простые утверждения.

(упрощенные) государственные объекты определяются следующим образом [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

Я сделал функционирующий стековый машинный парсер для этой грамматики (без тегов группы) путем переноса реализации C реализации Thompson NFA в 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].

в проблема

проблема возникает, когда я перехожу к следующему шагу: разбор операторов " | " или. Как это должно работать, строку:

-- "(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 для получения списка вместо этого изменяет что-либо, потому что каждая запись все еще получить то же выражение). Моя попытка была:--14-->

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

но это не удается ввести check [5]. Я ожидал бы, что это будет правильно, потому что p_regex должен иметь встроенный (состояние a) объект, подаваемый ему, и построение "Литеральных" цепочек с p_rec_many также работает таким образом.

возможно, я должен использовать buildExpressionTable? Это может помочь с этой конкретной проблемой, потому что я мог бы сделать ('$'eof) наивысший приоритет. Я попытался это сделать, но не представляю, как справлюсь с такими вещами, как star plus и операторы вопросительных знаков, поскольку все они должны ссылаться на себя.

(EDIT: я снова попытался использовать buildExpressionTable, и мне кажется, что это было бы слишком упрощенно для того, что я хочу сделать. Он не может изначально обрабатывать сложенные постфиксные операторы [например, " a?* "], и мой план сделать" ' $ ' eof " наивысшим приоритетом также не будет работать, потому что это будет прикрепляться только к последнему проанализированному "термину", а не ко всей строке. Даже если бы я мог это сделать, оператор " $ " был бы применен назад: он должен быть самым последним проанализированным термином и подаваться к предыдущему термину. Чем больше я работаю с этим, тем больше я задаюсь вопросом, не следует ли мне просто отменить строку выражения перед ее разбором.)

вопрос

Итак, что я делаю не так? Я уверен, что есть способ сделать то, что я пытаюсь сделать, я просто не пока не могу понять. Спасибо, что уделили мне время.

сноски

[1] Если вы хотите увидеть именно то, что я на самом деле можно найти здесь.

[2] идея тегов Open/CloseGroup заключается в отслеживании совпадений групп во время выполнения NFA. Размещение в приведенном выражении может быть не совсем правильным, но этот способ будет работать правильно, если обнаруженные теги CloseGroup только создают группу захвата, если соответствующая OpenGroup найдена (т. е. В приведенном выше примере мы только создадим захват, если хотя бы один " b " был замечен).

все другие конструкции тегов верны, и я проверил, что этот NFA соответствует строкам, как ожидалось.

[3] реализация Томпсона описана здесь и мой порт его можно увидеть здесь. Это отлично строит подмножество NFA, но в результирующей структуре каждое следующее состояние будет обернуто в справедливое. Это потому, что я ничего не использую для представления болтающихся указателей, а следующий шаг исправит правильный следующий шаг. Я мог бы преобразовать эту структуру в указанную выше, преобразовав все записи (только состояние) в записи (состояние), но это будет третий проход. Эта реализация уже требует первого прохода для преобразования регулярного выражения в постфиксную нотацию.

[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

2 ответов


вы, вероятно, не получаете много ответов, потому что это огромный вопрос, который требует чтения огромной бумаги, прежде чем кто-либо сможет подумать о написании ответа.

сказав это, по странному совпадению, я просто пытаюсь построить NFAs из regexes сам на этой неделе. ;-)


Итак,немедленно


Ok, поэтому вопрос был в основном: учитывая рекурсивную структуру данных (определенную в вопросе), как я могу создать парсер, который построит мое выражение за один проход. Моя первая попытка была своего рода" прикладной " по своей природе. Я смог создать рекурсивную структуру при условии, что не было условного ветвления. Но для разбора регулярных выражений требуется ветвление, поэтому мой подход не будет работать для 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

и это действительно создает структуру, в которой я нуждаюсь. Поэтому, если кто-то еще столкнется с подобной проблемой в будущем, возможно, этот вопрос/ответ может быть полезен.