2009-02-10 4 views
18

Мы использовали ANTLR для создания анализатора для грамматики, подобной SQL, и, хотя результаты в большинстве случаев являются удовлетворительными, есть несколько краевых случаев, которые нам нужно исправить; и поскольку мы сами не писали синтаксический анализатор, мы не очень хорошо это понимаем, чтобы иметь возможность делать разумные изменения.Каков наилучший способ написать парсер вручную?

Итак, мы хотели бы написать собственный парсер. Каков наилучший способ написать парсер вручную? Какой парсер следует использовать - рекурсивный спуск рекомендуется; это правильно? Мы будем писать его на C#, поэтому любые учебники для написания парсеров на этом языке были бы с благодарностью получены.

UPDATE: Меня также интересовали бы ответы, связанные с F # - Я искал причину использования этого в проекте.

+0

Саймон, я просматриваю ваши сообщения, и вы упоминаете, что вы «решили сделать это вручную». Вы интересуетесь упражнением здесь, чтобы узнать о синтаксическом анализе, или вы после семантически правильного, поддерживаемого, быстрого парсера для использования? Если последнее, я считаю, что ваше решение было преждевременным. Вы будете так привязаны к логике синтаксического анализа, что вы скоро забудете о «немногих случаях края», которые вы решили исправить. –

+0

Последний. Мы пошли по маршруту генератора-генератора и оказались с тем, чего мы не понимаем, и поэтому не можем исправить. Я бы предпочел, чтобы на месяцы работы было что-то, но которое можно исправить, чем что-то быстрое, чего нет. – Simon

+1

Я немного озадачен этим. Вы недостаточно знакомы с тем, как работают генераторы парсеров? Вы не должны играть с сгенерированным кодом вообще, если вы сделали это правильно. – Eric

ответ

16

Я очень рекомендую F # язык как ваш язык выбора для синтаксического анализа на платформе .NET. Корни в семействе языков ML означают, что он отлично поддерживает языковое программирование.

Дискриминационные союзы и сопоставление образцов позволяют получить очень сжатую и мощную спецификацию вашего АСТ. Функции более высокого порядка позволяют определять операции разбора и их состав. Первоклассная поддержка монадических типов позволяет управлять государственным управлением, неявно значительно упрощая состав парсеров. Мощный тип-вывод значительно помогает определению этих (сложных) типов. И все это можно указать и выполнить в интерактивном режиме, что позволяет быстро прототипировать.

Стефан Тольксдорф поставил это на практике его синтаксического анализа библиотеки комбинатора FParsec

Из его примеров мы видим, как естественно задана АСТ:

type expr = 
    | Val of string 
    | Int of int 
    | Float of float 
    | Decr of expr 

type stmt = 
    | Assign of string * expr 
    | While of expr * stmt 
    | Seq of stmt list 
    | IfThen of expr * stmt 
    | IfThenElse of expr * stmt * stmt 
    | Print of expr 

type prog = Prog of stmt list 

реализация парсера (частично Опущенные) является так же, как сжатое:

let stmt, stmtRef = createParserForwardedToRef() 

let stmtList = sepBy1 stmt (ch ';') 

let assign = 
    pipe2 id (str ":=" >>. expr) (fun id e -> Assign(id, e)) 

let print = str "print" >>. expr |>> Print 

let pwhile = 
    pipe2 (str "while" >>. expr) (str "do" >>. stmt) (fun e s -> While(e, s)) 

let seq = 
    str "begin" >>. stmtList .>> str "end" |>> Seq 

let ifthen = 
    pipe3 (str "if" >>. expr) (str "then" >>. stmt) (opt (str "else" >>. stmt)) 
      (fun e s1 optS2 -> 
       match optS2 with 
       | None -> IfThen(e, s1) 
       | Some s2 -> IfThenElse(e, s1, s2)) 

do stmtRef:= choice [ifthen; pwhile; seq; print; assign] 


let prog = 
    ws >>. stmtList .>> eof |>> Prog 

на второй линии, в качестве примера, и stmtch - синтаксические анализаторы, а sepBy1 - это комбинатор синтаксического анализатора, который принимает два простых синтаксических анализатора и возвращает парсер комбинаций. В этом случае sepBy1 p sep возвращает парсер, который анализирует один или несколько вхождений p, разделенных sep. Таким образом, вы можете увидеть, как быстро мощный синтаксический анализатор можно объединить с простыми парсерами. Поддержка F # для переопределенных операторов также допускает краткую инфиксную нотацию, например. комбинатор последовательности и комбинатор выбора могут быть указаны как >>. и <|>.

удачи,

Дэнни

1

В C/Unix традиционным способом является использование lex и yacc. С GNU эквивалентными инструментами являются flex и bison. Я не знаю, для Windows/C#.

+1

Я хотел бы написать это вручную - без использования генератора синтаксического анализатора. – Simon

+0

flex, в то время как GPLed, не является проектом GNU. – Eric

7

Рекурсивный спуск даст вам самый простой способ пойти, но я должен согласиться с mouviciel, что гибкий и бизон и, безусловно, стоит учиться. Когда вы узнаете, что у вас есть ошибка в вашей грамматике, определение определения языка в flex/bison будет намного проще, чем переписать ваш рекурсивный код спуска.

FYI парсер C# написан рекурсивным спуском, и он имеет тенденцию быть довольно надежным.

+3

Ада намного проще - это преувеличение. Методами рекурсивного парсера спуска являются грамматика (и ее простая, как только это признается) ... –

+1

Зависит от того, где ваша ошибка. С рекурсивным спусками вы можете семантически изменить значение целого раздела грамматики. В определении flex/bison это приведет к изменению 1 строки. В рекурсивном спуске вам, возможно, придется переписать половину кода, чтобы справиться с ним. – Spence

0

Ну, если вы не возражаете, используя другой инструмент компилятор компилятор как ANTLR Я предлагаю вам взглянуть на Coco/R

Я использовал его в прошлом, и это было очень хорошо ...

+0

Нет, мы в значительной степени решили написать один за другим. Спасибо хоть. – Simon

10

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

Если вы используете парсер RD, вы должны убедиться, что грамматика SQL не является леворекурсивной (и, при необходимости, исключает рекурсию), а затем в основном записывает функцию для каждого правила грамматики. См. this для получения дополнительной информации.

3

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

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

Пример:

Data = Object | Value; 
Value = Ident, '=', Literal; 
Object = '{', DataList, '}'; 
DataList = Data | DataList, Data; 


ParseData { 
    if PeekToken = '{' then 
    return ParseObject; 
    if PeekToken = Ident then 
    return ParseValue; 
    return Error; 
} 

ParseValue { 
    ident = TokenValue; 
    if NextToken <> '=' then 
    return Error; 
    if NextToken <> Literal then 
    return Error; 
    return(ident, TokenValue); 
} 

ParseObject { 
    AssertToken('{'); 
    temp = ParseDataList; 
    AssertToken('}'); 
    return temp; 
} 

ParseDataList { 
    data = ParseData; 
    temp = [] 
    while Ok(data) { 
    temp = temp + data; 
    data = ParseData; 
    } 
} 
1

Если бы я тебя, я бы еще раз попытать на ANTLRv3 с помощью графического интерфейса ANTLRWorks, который дает вам очень удобный способ проверки грамматики. Мы используем ANTLR в нашем проекте, и хотя кривая обучения может быть немного крутой в начале, как только вы узнаете, что это довольно удобно. Также в их бюллетене электронной почты есть много людей, которые очень полезны.

PS. IIRC у них также есть SQL-грамматика, на которую вы могли бы взглянуть.

НТН

+0

Спасибо, но я хотел бы написать его вручную. – Simon

6

рекурсивных парсеры действительно лучшие, может быть только парсеры, которые могут быть построены вручную. Вам все равно придется придираться к тому, что именно формальный, контекстно-свободный язык, и поместить ваш язык в нормальную форму. Я лично предложил бы удалить левую рекурсию и поместить ваш язык в Greibach Normal Form.Когда вы это делаете, синтаксический анализатор сам записывает себя.

Например, это производство:

A => aC 
A => bD 
A => eF 

становится чем-то простым, как:

int A() { 
    chr = read(); 
    switch char 
    case 'a': C(); 
    case 'b': D(); 
    case 'e': F(); 
    default: throw_syntax_error('A', chr); 
} 

И нет никаких гораздо более сложные случаи здесь (что труднее сделать, что ваша грамматика в правильной форме, но это позволяет вам контролировать, о котором вы упомянули).

Anton's Link также похоже хороший.

+1

+1 для нормальной формы Greibach - возвращает меня к дням Uni – pjp

+0

Я думаю, это предполагает, что язык LL (1). –

7

Добавление моего голоса в хор в пользу рекурсивного спуска (LL1). Они просты, быстры, и ИМО, совсем не трудно поддерживать.

Однако, взгляните на свой язык, чтобы убедиться, что он LL1. Если у вас есть какой-либо синтаксис, такой как C, например (((type)) foo) []), где вам, возможно, придется спускать несколько слоев круглых скобок, прежде чем вы узнаете, ищете ли вы тип, переменную или выражение, то LL1 будет очень сложным, а выигрыш снизу вверх.

+3

И правильная вещь - изменить язык на LL1 –

+1

@Stephan: Я склонен согласиться, хотя в один или два раза у меня не было выбора - мне пришлось разбирать C. –

+1

Или у вас просто логика в вашем синтаксическом анализаторе if (type) ... else if (variable) ... –

0

Причина, по которой вы не хотите, чтобы парсер, управляемый таблицами, заключался в том, что вы не сможете создавать разумные сообщения об ошибках. Это нормально для сгенерированного языка, но не для тех, в которых задействованы люди. Сообщения об ошибках, создаваемые c-подобными языковыми компиляторами, дают достаточные доказательства того, что люди могут адаптироваться к чему угодно, независимо от того, насколько они плохи.

2

Я предлагаю вам не писать lexer вручную - используйте flex или аналогичный. Задача распознавания жетонов не так уж трудно сделать, но я не думаю, что вы получите много.

Как уже говорилось, рекурсивные анализаторы спуска проще всего писать вручную. В противном случае вам нужно поддерживать таблицу состояний переходов для каждого токена, который на самом деле не читается человеком.

Я уверен, что ANTLR в любом случае реализует рекурсивный парсер спуска: есть упоминание об этом in an interview about ANTLR 3.0.

Я также нашел серию сообщений в блоге о writing a parser in C#. Это кажется довольно нежным.

0

Я также проголосую за использование существующего анализатора + лексера.

Единственная причина, я могу видеть, если делать это вручную:

  • если вы хотите что-то относительно простой (например, проверка/разбор некоторых входных)
  • , чтобы узнать/понять принципы.
0

JFlex - это гибкая реализация для Java, и теперь есть порт C# этого проекта http://sourceforge.net/projects/csflex/. Там также появляется порт C# CUP, который находится здесь: http://sourceforge.net/projects/datagraph/

Я также рекомендовал бы избежать ручной обработки вашего собственного решения. Я попробовал это однажды сам для очень простого языка (часть университетского проекта), и это было невероятно трудоемким и сложным. Также чрезвычайно сложно поддерживать и изменять после написания.

Использование существующего генератора синтаксиса - это путь, так как основная часть тяжелой работы была выполнена и прошла испытания на протяжении многих лет.

+0

Если ваш код «похож» на EBNF, то ваш код лишь немного сложнее поддерживать, чем EBNF. Всегда нужно начинать с грамматики при создании парсера или изменении его. –

0

Посмотрите на генераторы gplex и gppg, lexer и parser для .NET. Они хорошо работают и основаны на том же (и почти совместимом) вводе как lex и yacc, который относительно прост в использовании.

2

Опасность оскорбления ОП, написание синтаксического анализатора для большого langauge, как у конкретного SQL-агента поставщика вручную, когда доступны инструменты генератора синтаксического анализатора (такие как ANTLR), просто сумасшедшие. Вы потратите гораздо больше времени на переписывание своего парсера вручную, чем исправление «крайних случаев» с помощью генератора парсера, и вам всегда придется вернуться и пересмотреть парсер в любом случае по мере того, как стандарты SQL будут двигаться или вы обнаружите, что вы неправильно поняли что-то другое. Если вы не понимаете свою технологию синтаксического анализа достаточно хорошо, потратьте время, чтобы понять это. Вам не зайдут месяцы, чтобы понять, как бороться с крайними случаями с генератором синтаксического анализатора, и вы уже признались, что готовы потратить месяцы на это вручную.

Сказав это, если вы исправились, переработав его вручную, использование рекурсивного парсера спуска - лучший способ сделать это вручную.

(Примечание: OP выяснены ниже, что он хочет эталонную реализацию ИМХО Вы никогда не должны кодировать эталонную реализацию грамматики процедурно, поэтому рекурсивный спуск выходит.).

+1

Без обид. Переписывание парсера в качестве изменений стандартов здесь не является проблемой; это наша собственная SQL-подобная грамматика, и парсер будет эталонной реализацией. – Simon

+0

Если его ссылочная реализация затем НЕ кодирует рекурсивный парсер спуска. Парнеры RD - это все процедурный код, и это намного сложнее понять, чем спецификация. Вам абсолютно нужна чистая, чистая спецификация вашего langauge в четко определенных правилах langauge, чтобы другие («refererence», помните?) Могли ясно видеть, что было указано. ANTLR намного лучше для этого, чем RD. Bison, использующий опцию GLR, будет лучше, чем ANTLR, потому что вы можете безнаказанно кодировать контекстно-свободные правила грамматики. –

+0

+1 для точки относительно эталонных реализаций. Используя генератор парсера, вы проверяете опубликованную грамматику и избегаете проблем, которые ваш (ручной) парсер фактически не реализует грамматику. –

2

Там нет "один лучший" способ.В зависимости от ваших потребностей вам может понадобиться снизу вверх (LALR1) или рекурсивный спуск (LLk). Статьи such as this one приводят личные причины для предпочтения LALR (1) (снизу вверх) до LL (k). Однако каждый тип анализатора имеет свои преимущества и недостатки. Обычно LALR будет быстрее, так как в качестве таблицы поиска создается finite-state_machine.

Чтобы выбрать то, что подходит вам, изучите вашу ситуацию; ознакомьтесь с инструментами и технологиями. Начиная с некоторых LALR и LL статьи в Википедии - неплохой выбор. В обоих случаях вы должны ВСЕГДА начинать с указания грамматики в BNF или EBNF. Я предпочитаю EBNF за его лаконичность.

Как только вы осознали, что хотите сделать, и как представить его в виде грамматики (BNF или EBNF), попробуйте несколько различных инструментов и запустите их через репрезентативные образцы текста, которые будут проанализированы ,

Занимательно:

Однако я слышал, что LL (K) является более гибким. Я никогда не удосужился узнать для себя. Из моих опытов по созданию парсеров я заметил, что если LALR или LL (k) лучший способ выбрать то, что лучше для ваших нужд, это начать с написания грамматики. Я написал свою собственную библиотеку шаблонов шаблонов парсеров для сценариев Java C++ EBNF, использовал Lex/YACC и закодировал небольшой парсер R-D. Это было распространено в течение большей части 15 лет, и я провел не более двух месяцев в самом длинном из трех проектов.

Смежные вопросы