vak: (Default)
[personal profile] vak
Про лексический сканер я уже писал, теперь черёд синтаксического парсера. Есть много современных инструментов для Си++, от ANTLR до PEGTL, но я покажу, как задействовать для этой задачи старый добрый YACC, точнее bison.

Будем решать простейшую задачу: подсчитывать количество букв и слов в тексте. Я нашёл этот пример в интернете и переиначил его на свой лад. Парсер делаем в виде класса Parser с двумя методами. Функция parse() разбирает входной поток и накапливает счётчики. Функция print() печатает результат. Всё это поместим в пространство имён Demo. Функция main() выглядит тривиально, файл main.cc:
#include "parser.hh"

int main()
{
Demo::Parser parser;
parser.parse();
parser.print();
return 0;
}
Объявим класс Parser, файл parser.hh:
#include "grammar.tab.hh"

namespace Demo {

class Parser {
unsigned chars, words, lines, uppercase, lowercase;

int get_next_token(Grammar::semantic_type *const lval,
Grammar::location_type *location);
public:
void parse();
void print();

friend class Grammar;
};

} // namespace Demo
Результат обработки входного потока будем накапливать в счётчиках chars, words и прочее. Функция get_next_token() реализует лексический сканер: выдаёт следующий токен из входного потока. В этом примере для простоты я сделал сканер без применения flex, чтобы не затуманивать суть дела.

Бизон будет порождать код в виде класса Grammar. Чтобы из грамматики иметь прямой доступ к счётчикам в классе Parser, я объявляю Grammar как friend class.

Реализуются методы класса Parser довольно прямолинейным образом, файл parser.cc:
#include <iostream>
#include "parser.hh"

namespace Demo {

void Parser::parse()
{
chars = 0;
words = 0;
lines = 0;
uppercase = 0;
lowercase = 0;

Grammar grammar(*this);
if (grammar.parse() != 0) {
std::cerr << "Parse failed!\n";
}
}

void Parser::print()
{
std::cout << "Lines: " << lines << std::endl;
std::cout << "Words: " << words << std::endl;
std::cout << "Characters: " << chars << std::endl;
std::cout << "Uppercase: " << uppercase << std::endl;
std::cout << "Lowercase: " << lowercase << std::endl;
}

int Parser::get_next_token(Grammar::semantic_type *lval, location *loc)
{
// Берём первый символ.
char c0;
std::cin.get(c0);
if (std::cin.eof()) {
return Grammar::token::END;
}
if (c0 == '\n') {
loc->lines();
return Grammar::token::NEWLINE;
}
if (!isalpha(c0)) {
return Grammar::token::CHAR;
}

// Проверяем следующий символ.
char c;
std::cin.get(c);
if (std::cin.eof() || !isalpha(c)) {
if (!std::cin.eof())
std::cin.unget();

if (isupper(c0)) {
return Grammar::token::UPPER;
} else {
return Grammar::token::LOWER;
}
}

// Проходим до конца слова.
std::string text;
text += c0;
for (;;) {
text += c;
std::cin.get(c);
if (!isalpha(c)) {
std::cin.unget();
break;
}
}
lval->build(text);
return Grammar::token::WORD;
}

} // namespace Demo
Функция get_next_token() считывает по одному символу из входного потока и классифицирует лексемы как буквы, слова и прочие символы. Она возвращает целочисленный тип токена, как определено в грамматике. Для слов, строка запоминается в первом параметре lval. При обнаружении конца строки номер строки увеличивается вызовом метода второго параметра loc.
 
Теперь собственно грамматика, файл grammar.yy:
%skeleton "lalr1.cc"
%require "3.2"
%debug
%defines
%define api.namespace { Demo }
%define api.parser.class { Grammar }
%define api.value.type variant
%define parse.assert
%locations

// Добавляем параметр для конструктора Grammar().
%code requires {
namespace Demo {
class Parser;
}
}
%parse-param { Demo::Parser &parser }

// Для чтения входного потока используем метод get_next_token().
%code {
#include "parser.hh"
#undef yylex
#define yylex parser.get_next_token
}

// Определяем нужные типы токенов.
%token END 0 "end of file"
%token UPPER
%token LOWER
%token <std::string> WORD
%token NEWLINE
%token CHAR

%%

list_option : END | list END;

list
: item
| list item
;

item
: UPPER {
// Единичная буква, прописная.
parser.uppercase++;
parser.chars++;
parser.words++;
}
| LOWER {
// Единичная буква, строчная.
parser.lowercase++;
parser.chars++;
parser.words++;
}
| WORD {
// Слово как последовательность букв.
parser.words++;
parser.chars += $1.length();
for (const char &c : $1) {
if (isupper(c)) {
parser.uppercase++;
} else {
parser.lowercase++;
}
}
}
| NEWLINE {
// Разделитель строки.
parser.lines++;
parser.chars++;
}
| CHAR {
// Другой символ.
parser.chars++;
}
;

%%

// В случае синтаксической ошибки выдаём сообщение.
void Demo::Grammar::error(const location_type &loc, const std::string &message)
{
std::cerr << "Error: " << message << " at " << loc << "\n";
}
Компилируем, при этом бизон из грамматики grammar.yy породит файлы grammar.tab.cc и grammar.tab.hh (ещё location.hh, но это отдельная тема):
bison -d grammar.yy
c++ -std=c++11 -c main.cc
c++ -std=c++11 -c parser.cc
c++ -std=c++11 -c grammar.tab.cc
c++ main.o parser.o grammar.tab.o -o wcount
Запускаем:
$ echo 123 fooo BAR | ./wcount
Lines: 1
Words: 2
Characters: 13
Uppercase: 3
Lowercase: 4
Вот такой простой способ создавать парсеры. При этом можно комбинировать bison и flex, что удобно, хотя и не обязательно.
This account has disabled anonymous posting.
If you don't have an account you can create one now.
HTML doesn't work in the subject.
More info about formatting

If you are unable to use this captcha for any reason, please contact us by email at [email protected]

OSZAR »