Разбор языка, похожего на TeX, с помощью lpeg

Я изо всех сил пытаюсь получить голову вокруг LPEG. Мне удалось создать одну грамматику, которая делает то, что я хочу, но я бился головой об этом и не ушел далеко. Идея состоит в том, чтобы разобрать документ, который является упрощенной формой TeX. Я хочу разделить документ на:

  • Среды, которые \begin{cmd} а также \end{cmd} пар.
  • Команды, которые могут принимать аргумент так: \foo{bar} или может быть голым: \foo,
  • И окружение, и команды могут иметь такие параметры: \command[color=green,background=blue]{content},
  • Другие вещи

Я также хотел бы отслеживать информацию о номере строки в целях обработки ошибок. Вот что у меня так далеко:

lpeg = require("lpeg")
lpeg.locale(lpeg)
-- Assume a lot of "X = lpeg.X" here.

-- Line number handling from http://lua-users.org/lists/lua-l/2011-05/msg00607.html
-- with additional print statements to check they are working.
local newline = P"\r"^-1 * "\n" / function (a) print("New"); end
local incrementline = Cg( Cb"linenum" )/ function ( a ) print("NL");  return a + 1 end , "linenum"
local setup = Cg ( Cc ( 1) , "linenum" )
nl = newline * incrementline
space = nl + lpeg.space

-- Taken from "Name-value lists" in http://www.inf.puc-rio.br/~roberto/lpeg/
local identifier = (R("AZ") + R("az") + P("_") + R("09"))^1
local sep = lpeg.S(",;") * space^0
local value = (1-lpeg.S(",;]"))^1
local pair = lpeg.Cg(C(identifier) * space ^0 * "=" * space ^0 * C(value)) * sep^-1
local list = lpeg.Cf(lpeg.Ct("") * pair^0, rawset)
local parameters = (P("[") * list * P("]")) ^-1

-- And the rest is mine

anything = C( (space^1 + (1-lpeg.S("\\{}")) )^1) * Cb("linenum") / function (a,b) return { text = a, line = b } end

begin_environment = P("\\begin") * Ct(parameters) * P("{") * Cg(identifier, "environment") * Cb("environment") * P("}") / function (a,b) return { params = a[1], environment = b } end
end_environment = P("\\end{") * Cg(identifier) * P("}") 

texlike = lpeg.P{
  "document";
  document = setup * V("stuff") * -1,
  stuff = Cg(V"environment" + anything + V"bracketed_stuff" + V"command_with" + V"command_without")^0,
  bracketed_stuff = P"{" * V"stuff" * P"}" / function (a) return a end,
  command_with =((P("\\") * Cg(identifier) * Ct(parameters) * Ct(V"bracketed_stuff"))-P("\\end{")) / function (i,p,n) return { command = i, parameters = p, nodes = n } end,
  command_without = (( P("\\") * Cg(identifier) * Ct(parameters) )-P("\\end{")) / function (i,p) return { command = i, parameters = p } end,
  environment = Cg(begin_environment * Ct(V("stuff")) * end_environment) / function (b,stuff, e) return { b = b, stuff = stuff, e = e} end
}

Это почти работает!

> texlike:match("\\foo[one=two]thing\\bar")
{
  command = "foo",
  parameters = {
    {
      one = "two",
    },
  },
}
{
  line = 1,
  text = "thing",
}
{
  command = "bar",
  parameters = {
  },
}

Но! Во-первых, я не могу заставить работать часть номера строки. Функция внутри incrementline никогда не уволен.

Я также не могу понять, как вложенная информация захвата передается в функции обработки (именно поэтому я разбросал Cg, C а также Ct полуслучайно над грамматикой). Это означает, что только один элемент возвращается из command_with:

> texlike:match("\\foo{text \\command moretext}")
{
  command = "foo",
  nodes = {
    {
      line = 1,
      text = "text ",
    },
  },
  parameters = {
  },
}

Я также хотел бы иметь возможность проверить, совпадают ли начало и конец среды, но когда я попытался это сделать, мои обратные ссылки из "начала" не были в объеме к тому времени, когда я достиг "конца". Я не знаю, куда идти отсюда.

1 ответ

Поздний ответ, но, надеюсь, он даст некоторое представление, если вы все еще ищете решение или вам интересно, в чем проблема.

Есть несколько проблем с вашей грамматикой, некоторые из которых может быть сложно определить.

Ваш прирост строки здесь выглядит неправильно:

local incrementline = Cg( Cb"linenum" ) / 
                      function ( a ) print("NL");  return a + 1 end, 
                      "linenum"

Похоже, вы хотели создать именованную группу захвата, а не анонимную группу. Обратный захват linenum по существу используется как переменная. Проблема в том, что это внутри анонимного захвата, linenum не будет обновляться должным образом - function(a) всегда будет получать 1 при вызове. Вы должны переместить закрытие ) до конца так "linenum" Включено:

local incrementline = Cg( Cb"linenum" / 
                      function ( a ) print("NL");  return a + 1 end, 
                      "linenum")

Соответствующая документация LPeg для Cg захватить.

Вторая проблема с вашим anything нетерминальное правило:

anything = C( (space^1 + (1-lpeg.S("\\{}")) )^1) * Cb("linenum") ...

Здесь есть несколько вещей, которые нужно соблюдать осторожность. Во-первых, названный Cg захват (от incrementline правило, как только оно исправлено) ничего не производит, если оно не находится в таблице или вы не отозвали его обратно. Вторым важным моментом является то, что он имеет временную область видимости как переменную. Точнее, его область действия заканчивается, когда вы закрываете его во внешнем захвате - как то, что вы делаете здесь:

C( (space^1 + (...) )^1)

К тому времени, когда вы ссылаетесь на его backcapture с * Cb("linenum")уже поздно - linenum Вы действительно хотите уже закрыли свою сферу.

Я всегда находил LPeg's re Синтаксис немного проще, поэтому вместо этого я переписал грамматику:

local grammar_cb =
{
  fold = pairfold, 
  resetlinenum = resetlinenum,
  incrementlinenum = incrementlinenum, getlinenum = getlinenum, 
  error = error
}

local texlike_grammar = re.compile(
[[
  document    <- '' -> resetlinenum {| docpiece* |} !.
  docpiece    <- {| envcmd |} / {| cmd |} / multiline
  beginslash  <- cmdslash 'begin'
  endslash    <- cmdslash 'end'
  envcmd      <- beginslash paramblock? {:beginenv: envblock :} (!endslash docpiece)*
                 endslash openbrace {:endenv: =beginenv :} closebrace / &beginslash {} -> error .
  envblock    <- openbrace key closebrace
  cmd         <- cmdslash {:command: identifier :} (paramblock? cmdblock)?
  cmdblock    <- openbrace {:nodes: {| docpiece* |} :} closebrace
  paramblock  <- opensq ( {:parameters: {| parampairs |} -> fold :} / whitesp) closesq
  parampairs  <- parampair (sep parampair)*
  parampair   <- key assign value
  key         <- whitesp { identifier }
  value       <- whitesp { [^],;%s]+ }
  multiline   <- (nl? text)+
  text        <- {| {:text: (!cmd !closebrace !%nl [_%w%p%s])+ :} {:line: '' -> getlinenum :} |}
  identifier  <- [_%w]+
  cmdslash    <- whitesp '\'
  assign      <- whitesp '='
  sep         <- whitesp ','
  openbrace   <- whitesp '{'
  closebrace  <- whitesp '}'
  opensq      <- whitesp '['
  closesq     <- whitesp ']'
  nl          <- {%nl+} -> incrementlinenum
  whitesp     <- (nl / %s)*
]], grammar_cb)

Функции обратного вызова прямо определены как:

local function pairfold(...)
  local t, kv = {}, ...
  if #kv % 2 == 1 then return ... end
  for i = #kv, 2, -2 do
    t[ kv[i - 1] ] = kv[i]
  end
  return t
end

local incrementlinenum, getlinenum, resetlinenum do
  local line = 1
  function incrementlinenum(nl)
    assert(not nl:match "%S")
    line = line + #nl
  end

  function getlinenum() return line end
  function resetlinenum() line = 1 end
end

Тестирование грамматики с нетривиальной текстоподобной строкой с несколькими строками:

  local test1 = [[\foo{text \bar[color = red, background =   black]{
  moretext \baz{
even 
more text} }


this time skipping multiple

lines even, such wow!}]]

Производит следующий AST в формате lua-таблицы:

{
  command = "foo",
  nodes = {
    {
      text = "text",
      line = 1
    },
    {
      parameters = {
        color = "red",
        background = "black"
      },
      command = "bar",
      nodes = {
        {
          text = "  moretext",
          line = 2
        },
        {
          command = "baz",
          nodes = {
            {
              text = "even ",
              line = 3
            },
            {
              text = "more text",
              line = 4
            }
          }
        }
      }
    },
    {
      text = "this time skipping multiple",
      line = 7
    },
    {
      text = "lines even, such wow!",
      line = 9
    }
  }
}

И второй тест для начальных / конечных сред:

  local test2 = [[\begin[p1
=apple,
p2=blue]{scope} scope foobar   
\end{scope} global foobar]]

Который, кажется, дает примерно то, что вы ищете:

{
  {
    {
      text = " scope foobar",
      line = 3
    },
    parameters = {
      p1 = "apple",
      p2 = "blue"
    },
    beginenv = "scope",
    endenv = "scope"
  },
  {
    text = " global foobar",
    line = 4
  }
}
Другие вопросы по тегам